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).
Related
I am wondering how i can set the nav bar color for current VC only, without using viewWillAppear and viewWillDisappear pairs. These pair of functions are hard to maintain because it split the logic into 2 places.
I have seen multiple SO answers, but they change the whole navigation bar item throughout all VCs in the nav stack.
Two different solutions comes to my mind.
Implement custom NavigationBar for that ViewController
Use a base ViewController to override NavigationBar color in each controller
I would prefer the second way, and here is an example:
class BaseViewController: UIViewController {
// Set one color as default and override this property in each view controller that you want to change navigation bar color
var navigationBarColor: UIColor {
return .red
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barTintColor = navigationBarColor
}
}
class FirstViewController: BaseViewController {
override var navigationBarColor: UIColor {
return .blue
}
// Do your stuff
}
class SecondViewController: BaseViewController {
override var navigationBarColor: UIColor {
return .green
}
// Do your stuff
}
I want to implement a scrollToTop method on all of my viewControllers in my UITabBarController. The following is a method in the UITabBarControllerDelegate and triggers, when I select a tab.
The problem is, that I only want to scroll to the top of the viewController, when the viewController is active. So that the user can switch tabs without losing the scroll position, but when he touches the tab in the tabBar of the currently active tab, it should scroll to the top.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewControllerThatIsCurrentlyActiveInTabBar == viewController {
scrollToTop()
}
}
Basically, I need that condition of the if statement above.
I tried: viewController.isViewLoaded, tabBarController.selectedViewController == viewController, viewController.isBeingPresented. None of those conditions worked. It would either not trigger scrollToTop() or it would trigger always so that you lose the scroll position when you change tabs because it would immediately scroll to the top.
You need to make a code in should select instead of didselect. As it is unable to find the previous controller after selection. below is the example code for it.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.selectedViewController == viewController {
print("Same viewcontroller")
}
return true
}
Can you use below extension for getting top viewcontroller of tabbarcontroller.
extension UIViewController {
var top: UIViewController? {
if let controller = self as? UINavigationController {
return controller.topViewController?.top
}
if let controller = self as? UISplitViewController {
return controller.viewControllers.last?.top
}
if let controller = self as? UITabBarController {
return controller.selectedViewController?.top
}
if let controller = presentedViewController {
return controller.top
}
return self
}
}
You can use above extension below
if let rootViewController = UIApplication.top() {
//do with Active view controller
}
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?
I want to edit the status bar style of my application. However, I'm unable to edit it from my main UIViewController. So I think multiple status bars settings are stacked because of embed UIViewControllers.
Here is how I initiate my navigationController in the didFinishLaunchingWithOptions method:
let navigationVC = CustomNavigationController(rootViewController: MenuInstance)
navigationVC.setNavigationBarHidden(true, animated: false)
Then, I move some views of other UIViewControllers in the menu UIViewController (MenuInstance) for any reason like this:
let scannerVC = ScannerViewController()
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(scannerVC)
scannerVC.didMove(toParentViewController: self)
}
I tried to create class to set prefersStatusBarHidden = true
class ModalViewViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
I also created a class for the UINavigationController
class CustomNavigationController: UINavigationController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
The goal of trying to remove the status bar is to find where the status bar comes from. I want only one status bar in the MenuInstance UIViewController that I can edit. The fact that some views of UIViewControllers are embed in one UIViewController makes me confused.
If the MenuInstance view controller is the root view controller of the navigation controller, then the MenuInstance's implementation of prefersStatusBarHidden is all that matters. No other view controller's preference is consulted. The "embed" stuff is irrelevant (unless you want to make it relevant).
class MenuInstance : UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
}
Note, however, that this will not work on an iPhone X. You cannot hide the status bar on an iPhone X.
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.