I have a BaseViewController and a SideMenu that uses my MenuViewController. There are many possible "Home" screens that all inherit from this same BaseViewController. MenuViewController also inherits from BaseViewController.
I would like an overlay to be shown on the home screen and then disappear when the Menu is no longer in focus. So far, I can only get the overlay to show, but not disappear.
The overlay disappears if I tap one of the menu items, which performs a segue to the appropriate subclass of BaseViewController (for example, the Home screen or Settings screen). This effectively refreshes the screen, and I think I could keep a reference to the caller and segue back to it if I can't find a better solution.
Things I have tried:
overlay.removeFromSuperview()
view.sendSubview(toBack: overlay)
overlay.isHidden = true
overlay.alpha = 0.0
Moving hideOverlay() into MenuViewController.
Using super.overlay within MenuViewController instead of simply overlay or self.overlay.
I can confirm that all lines of code are hit with breakpoints, but the overlay view does not go away. BaseViewController's viewWillAppear() is not called when I tap to make the menu go away, because its subclass is already in view (just pushed to the side a bit).
Here is a minimal reproducible example:
BASE VIEW CONTROLLER
import UIKit
import SideMenu
class BaseViewController: UIViewController {
let overlay = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// Setup
overlay.frame = self.view.frame
overlay.backgroundColor = UIColor.clear
overlay.alpha = 0.5
overlay.isUserInteractionEnabled = false
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(overlay)
}
// WORKS
func showMenu() {
// menuLeftNavigationController is MenuViewController.
self.present(SideMenuManager.menuLeftNavigationController!, animated: true) {
UIView.animate(withDuration: 0.2) {
self.overlay.backgroundColor = Constants.Colors.overlayColor // Already defined.
}
// PROBLEM IS HERE
func hideOverlay() {
UIView.animate(withDuration: 0.2) {
self.overlay.backgroundColor = UIColor.clear
self.overlay.setNeedsLayout()
self.overlay.layoutIfNeeded()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
}
MENU VIEW CONTROLLER
import UIKit
import SideMenu
class MenuViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Tableview boilerplate
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
// BREAKPOINTS CONFIRM THIS CODE EXECUTES.
override func viewWillDisappear(_ animated: Bool) {
self.hideOverlay()
}
}
In viewWillDisappear when you call self.hideOverlay, you're calling that on your MenuViewController.
When showMenu() is called, you present the MenuViewController and then set the overlay background colour on the presenting view controller.
I guess, what you want to do here is in the completion of the MenuViewController, dismiss method do:
if let presentingViewController = self.presentingViewController as? BaseViewController {
presentingViewController.hideOverlay()
}
Hopefully that makes sense?
Related
I created a translucent navigation bar and set the tint color of it to white. However, there's a specific VC containing a map and a white back button on the map is not quite visible sometimes.
Therefore, I created a back button image with shadow and use navigationController?.navigationBar.backIndicatorImage to set it in viewWillAppear of that VC and set back the normal image when the VC is not on top of the stack through
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
viewController.viewWillAppear(animated)
}
of the previous VC. That works fine.
However, when I test to span halfway back from the map VC to the previous VC but then release the spanning so that the map VC does not get dismissed but still triggers viewWillAppear of the previous VC, at this time the backIndicatorImage is set to normal image, which I do not expect.
How could I achieve the goal? Or is there any way to set the drop shadow on the back button of UINavigationBar for only a specific VC in UINavigationController?
UINavigationControllerDelegate has a method called navigationController(_:willShow:animated:) which you could use to animate alongside the navigation controller push/pop animation.
In short, you should subclass UINavigationController and make it a delegate of itself:
import UIKit
class AppNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension AppNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
guard animated, let vc = viewController as? ShadowBackButtonProtocol, vc.shouldAddShadowToImage, let image = vc.myImage else {
return
}
navigationController.transitionCoordinator?.animate(alongsideTransition: { (context) in
if vc.shouldAddShadowToImage {
// Add your shadow
} else {
// Remove the shadow
}
}, completion: nil)
}
}
Of course, both view controllers should have to implement ShadowBackButtonImage:
protocol ShadowBackImageButton {
var shouldAddShadowToImage: Bool { get }
var myImage: UIImage? { get }
}
extension MyViewController: ShadowBackImageButton {
var shouldAddShadowToImage: Bool { return true }
var myImage: UIImage? { return navigationController?.navigationBar.backIndicatorImage }
}
extension AnotherSubclassOfViewController: ShadowBackImageButton {
var shouldAddShadowToImage: Bool { return false }
var myImage: UIImage? { return navigationController?.navigationBar.backIndicatorImage }
}
If you ask me, I would prefer to add a UIView over the view controller that has to add a shadow and simply mimick the UINavigationBar appearance, as navigationController(_:willShow:animated) has had some issues when performing the back animation (that is probably a bug).
I followed this tutorial to smoothly hide the statusBar smoothly hide statusBar and everything works fine when I use it on practice projects. I use the code in other project's that do not have SplitVC but have a tabBar and uses a navVC & tableView and everything works fine. In those I can successfully make it appear/disappear.
In my actual project I'm using a SplitViewController for iPad. I noticed when I implemented the directions from the link to my SplitViewController the statusBar wouldn't hide. I then made a new project using Apple's default MasterDetailApp to make sure I wasn't doing anything wrong but it doesn't work there either. I kept all of Apple's original code and only added in the necessary methods to make the statusBar appear/disappear
in info.plist I added the View controller-based status bar appearance and set it to YES
in storyboard I added a purple button to the DetailVC to trigger the statusBar disappearance. I also added in the method to make the backBar button disappear/reappear
I added all the methods to make the statusBar disappear/disappear to the DetailVC scene.
I added a tapGesture to the scene to make the statusBar and backButton reappear
I clicked the Plus button on the Master scene, a date appeared, clicked it to get to the DetailVC, pressed the purple buttonPressed to hide the statusBar and backButton but only the backButton gets hidden. I touch the background and the backButton reappears. The statusBar doesn't move.
I kept all the original code from Apple's project's and added mines below it:
class DetailViewController: UIViewController {
//MARK:- Apple's code
#IBOutlet weak var detailDescriptionLabel: UILabel!
func configureView() {
if let detail = detailItem {
if let label = detailDescriptionLabel {
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
// make backButton and statusBar reappear when scene is tapped
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(showBackButtonAndStatusBar))
view.addGestureRecognizer(tapGesture)
}
var detailItem: NSDate? {
didSet {
configureView()
}
}
//MARK:- Outside of the tapGesture in viewDidLoad everything below here is what I added
// bool to determine wether to hide the statusBar or not
var statusBarShouldBeHidden = false
// api method to allow the staus bar to be hidden
override var prefersStatusBarHidden: Bool{
return statusBarShouldBeHidden
}
// api method to animate status bar appearance/disappearance
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func buttonTapped(_ sender: UIButton) {
// 1. hide backBar button
navigationItem.setHidesBackButton(true, animated: false)
// 2. set bool to true
statusBarShouldBeHidden = true
UIView.animate(withDuration: 0.25){
// 3. api method to allow the statusBar to disappear
self.setNeedsStatusBarAppearanceUpdate()
}
}
//called when background is touched and added to tapGesture in viewDidLoad
#objc func showBackButtonAndStatusBar(){
// 1. set bool to false
statusBarShouldBeHidden = false
UIView.animate(withDuration: 0.25){
// 2. bring statusBar back
self.setNeedsStatusBarAppearanceUpdate()
}
// 3. bring backButton back
navigationItem.setHidesBackButton(false, animated: true)
}
}
How can I get the SplitViewVC to let me hide the statusBar?
It appears that you are trying to hide the status bar through the detail view controller. The status bar in the user interface is controlled only by the split view controller because it is on top of the view controller hierarchy. Therefore, the easiest way to control the behavior of the status bar is to subclass UISplitViewController and then override the prefersStatusBarHidden computed property in the subclass. Also, make sure you go to your storyboard and change the split view controller's custom class field in the Identity inspector to your subclass.
---Updated Answer---
#LanceSamaria Okay, I took your code above and tweaked some things. First of all, I only added the button action and not the tap gesture. Also, I commented out the hiding the back button, because this is important in the UI in order to be able to go back to the master view. Anyway, now when you click the button, the SplitViewController will hide the status bar. If you click the button again, then the status bar will reappear.
import UIKit
class DetailViewController: UIViewController {
#IBOutlet weak var detailDescriptionLabel: UILabel!
var statusBarShouldBeHidden = false
func configureView() {
// Update the user interface for the detail item.
if let detail = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}
/* override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
} */
var detailItem: NSDate? {
didSet {
// Update the view.
self.configureView()
}
}
#IBAction func buttonTapped(_ sender: UIButton) {
// 1. hide backBar button
//navigationItem.setHidesBackButton(true, animated: false)
// 2. set bool to true
statusBarShouldBeHidden = !statusBarShouldBeHidden
UIView.animate(withDuration: 0.25){
// 3. api method to allow the statusBar to disappear
guard let svc = self.splitViewController as? SplitViewController else { return }
svc.statusBarShouldBeHidden = self.statusBarShouldBeHidden
svc.setNeedsStatusBarAppearanceUpdate()
}
}
}
Also, one more really important thing. Below is my code for the split view controller subclass. Note that I use the same variable name "statusBarShouldBeHidden" in both the split view controller and the detail controller.
import UIKit
class SplitViewController: UISplitViewController {
var statusBarShouldBeHidden = false
override func viewDidLoad() {
super.viewDidLoad()
}
override var prefersStatusBarHidden: Bool {
return statusBarShouldBeHidden
}
}
Thank you for posting this question. This has helped my learn a lot trying to solve this problem. Please let me know if you still have a question about what this.
I am Using Swift 3. I have searched about this and found the solution
navigationDrawerController?.TransitionFromRootViewController
but when I used this line it say TransitionFromRootViewController is not a function.
So I tried Using
navigationDrawerController?.transition(from: RootViewController(), to: destViewController(), duration: 0.2, options: .transitionCrossDissolve, animations: nil, completion: nil)
but it shows error that the:
"child view controller must have a common parent view controller when calling transitionfrom view controller"
Can anyone help me please? If someone can push an example of navigation drawer with the switching would be a great.
Here is the solution, which I posted to the NavigationDrawerController example project in the programmatic directory, Material 2.1.2.
It shows how to transition with multiple navigation controllers, and by itself.
import UIKit
import Material
class LeftViewController: UIViewController {
private var transitionButton: FlatButton!
open override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Color.blue.base
prepareTransitionButton()
}
#objc
internal func handleTransitionButton() {
// Transition the entire NavigationDrawer rootViewController.
// navigationDrawerController?.transition(to: TransitionedViewController(), completion: closeNavigationDrawer)
// Transition the ToolbarController rootViewController that is in the
// NavigationDrawer rootViewController.
(navigationDrawerController?.rootViewController as? ToolbarController)?.transition(to: TransitionedViewController(), completion: closeNavigationDrawer)
}
internal func closeNavigationDrawer(result: Bool) {
navigationDrawerController?.closeLeftView()
}
private func prepareTransitionButton() {
transitionButton = FlatButton(title: "Transition VC", titleColor: Color.white)
transitionButton.addTarget(self, action: #selector(handleTransitionButton), for: .touchUpInside)
view.layout(transitionButton).horizontally().center()
}
}
You can find a reference to the discussion in GitHub issue-546
All the best!
I'm using the SwipeViewController.
It's a sub class of UINavigationController, UIPageViewControllerDelegate, UIScrollViewDelegate
The view is instantiated within the AppDelegate.Swift by:
let pageController = UIPageViewController(transitionStyle: .Scroll, navigationOrientation: .Horizontal, options: nil)
let navigationController = ViewController(rootViewController: pageController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
The complete code of the ViewController is:
import UIKit
import SwipeViewController
class ViewController: SwipeViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.hidden = true
let VC1 = UIViewController()
VC1.view.backgroundColor = UIColor(red: 0.19, green: 0.36, blue: 0.60, alpha: 1.0)
VC1.title = "Recent"
let VC2 = UIViewController()
VC2.view.backgroundColor = UIColor(red: 0.70, green: 0.23, blue: 0.92, alpha: 1.0)
VC2.title = "Random"
setViewControllerArray([VC1, VC2])
setFirstViewController(0)
}
}
My problem is, once the App is started, no matter if in simulator or physical device. There is a gap in the exact height of the status bar.
Once I press on the screen, the view "moves up".
Also. For only one time I'm able to grab the view and grab it down. Then the screen will "run back" to the top (like in a graphic bug) and from that moment on, till next App start, the screen is "locked" to the window. A condition I want all the time....
What is the best practise to delete those "bugs", have the NavigationBar hidden and the views locked to the window? Help is very appreciated.
PS. If I don't hide the NavigationBar, there is no bug at all.
PPS. You can download the sample project from the provided github link. It's exactly the same.
Use viewWillAppear instead of viewDidLoad, it will execute the code before the finish creating the view
override func viewWillAppear(animated: Bool) {
self.navigationController?.navigationBarHidden = true
}
This is added since In iOS 9, XCode 7, Swift 2.0
override func prefersStatusBarHidden() -> Bool {
return true
}
To solve the similar problem I've created singleton class to implement UINavigationControllerDelegate. And implemented following method:
public func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if (viewController.isMemberOfClass:(YourUIViewControllesSubclass1.self)){
//or another detection
navigationController.navigatonBarHidden = true//or false if needed
} else if (viewController.isMemberOfClass:(YourUIViewControllesSubclass2.self)){
navigationController.navigatonBarHidden = true//or false if needed
}/ ... may check others
I wish it would solve the problem
I found the solution. The following function has to be overwritten in the UINavigationController Subclass (SwipeViewController.swift).
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
if self.view.frame.size.height == UIScreen.mainScreen().bounds.height {
for childVC in childViewControllers {
childVC.view.frame = CGRectMake(0, navigationBar.frame.size.height, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height - navigationBar.frame.size.height);
}
}
}
EDIT
I think my problem is that I add the views as subviews in the same view, thats why I can't remove it ?
Im trying to learn swiping between views using XIB.
My storyboard contains 3 views
-Login
-Create Account
-View with scrollview that scrolls between a tableview and a blank view. This view has an embedded navigation controller (Editor -> Embed In -> Navigation Controller)
I Don't want the navigation controller to be shown in my blank page.
I have created the tableView Controller and the blank UIControllerView by adding them as "addChildViewController", See code below
import UIKit
class MasterViewForScroll: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
let Inbox : FriendlistTableBarView = FriendlistTableBarView(nibName: "FriendlistTableBarView", bundle: nil)
let Camera : CameraViewController = CameraViewController(nibName: "CameraViewController", bundle: nil)
func creatingSubViews() {
self.addChildViewController(Inbox)
self.scrollView.addSubview(Inbox.view)
Inbox.didMoveToParentViewController(self)
Inbox.navigationController?.navigationBar.hidden = false
var CameraView = Camera.view.frame
CameraView.origin.x = self.view.frame.width
Camera.view.frame = CameraView
self.addChildViewController(Camera)
self.scrollView.addSubview(Camera.view)
Camera.didMoveToParentViewController(self)
self.scrollView.contentSize = CGSizeMake(self.view.frame.width * 2, self.view.frame.height)
}
override func viewDidLoad() {
super.viewDidLoad()
creatingSubViews()
}
So my question is: How do I hide the navigation controller in the "Camera" view.
Thank you
In your CameraViewController class, add the following code:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBarHidden = true
}
Next, in your FriendlistTableBarView class (I guess it is a UIViewController subclass), add the following code:
override func viewWillAppear() {
super.viewWillAppear()
self.navigationController?.navigationBarHidden = false
}
So, when you swipe to the right - navigation bar will hide, and when you swipe to the left - it will appear again.