iOS 14 navigation bar title is not displayed in UINavigationController created programmatically - ios

iOS 14.2, when I tried to present a NavigationController controller programmatically with the code snippet below.
#objc private func handleClick() {
let viewController = MyViewController()
self.present(viewController, animated: true, completion: nil)
}
The bar title in the new controller won't get rendered. Am I missing anything?
class MyViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "TEST" // NOT WORK
self.navigationItem.title = "Title" // NOT WORK
}
}
Also tried the code snippet below to nest a regular View Controller into an UINavigableController but the title is still not rendered.
#objc private func handleHelpClick() {
let innerVC = MyInnerViewController()
innerVC.title = "TEST"
let viewController = UINavigationController(rootViewController: innerVC)
self.present(viewController, animated: true, completion: nil)
}

The documentation says:
A navigation controller builds the contents of the navigation bar dynamically using the navigation item objects (instances of the UINavigationItem class) associated with the view controllers on the navigation stack.
https://developer.apple.com/documentation/uikit/uinavigationcontroller
So from my understanding you have to set the title for your UIViewController itself instead for the UINavigationController.
Example:
class MyViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class ViewController: UIViewController {
private lazy var button: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Display NavVC", for: .normal)
btn.setTitleColor(.blue, for: .normal)
btn.addTarget(self, action: #selector(displayNavVC), for: .touchUpInside)
return btn
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
configureButton()
}
private func configureButton() {
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
#objc
private func displayNavVC() {
let vc = UIViewController()
vc.title = "abc"
let navigationVC = MyViewController(rootViewController: vc)
self.present(navigationVC, animated: true, completion: nil)
}
}
Results in:

#finebel's answer in Playground
import UIKit
import PlaygroundSupport
class MyViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class ViewController: UIViewController {
private lazy var button: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Display NavVC", for: .normal)
btn.setTitleColor(.blue, for: .normal)
btn.addTarget(self, action: #selector(displayNavVC), for: .touchDown)
return btn
}()
override func viewDidLoad() {
super.viewDidLoad()
configureButton()
}
private func configureButton() {
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
#objc
private func displayNavVC() {
let vc = UIViewController()
vc.title = "abc"
let navigationVC = MyViewController(rootViewController: vc)
self.present(navigationVC, animated: true, completion: nil)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()

Related

Is there a way to resign first responder of view that isn't currently presented?

This is the situation:
The root view controller of a UINavigationController has a search bar. When a user searches something, they can tap on a cell that pushes a new view controller onto the navigation controller with some info. When this happens, the intended behavior is for the user to be able to go back and still see the keyboard open with the most recent search query populated within the search bar.
However, there is also a situation in which the user may tap on a tab bar that should pop all the view controllers off the navigation controller except the root controller. In that case, I would like to resign the search bar textField's first responder before the root controller is presented. If I try to do this in viewWillAppear(_:), resignFirstResponder() fails.
Is this possible? I've tried using delegation and notifications but nothing seems to work.
As #Sweeper said, endEditing worked for me, but resignFirstResponder didn't. I executed in viewWillAppear:
import PlaygroundSupport
import UIKit
class ViewController: UIViewController {
let button = UIButton()
let searchBar = UISearchBar()
override func viewDidLoad() {
super.viewDidLoad()
button.setTitle("Button", for: .normal)
button.tag = 1
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.backgroundColor = .black
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
self.navigationItem.titleView = searchBar
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
button.widthAnchor.constraint(equalToConstant: 200),
button.heightAnchor.constraint(equalToConstant: 100),
])
}
#objc func buttonPressed(sender: UIButton) {
if case let tag = sender.tag, tag == 1 {
let secondVC = SecondVC()
self.navigationController?.pushViewController(secondVC, animated: true)
}
}
}
class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .yellow
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.view.endEditing(true)
}
}
let navVC = UINavigationController(rootViewController: ViewController())
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = navVC

Present a viewController from collectionView

I am extending the UICollectionView to create an empty View, I couldn't call self.present. Is there a better way to handle empty view? How should i handle self.present in collectionView?
extension UICollectionView {
func setEmptyView() {
let signUpButton = UIButton(type: .custom)
signUpButton.addTarget(self, action: #selector(onSignIn), for: .touchUpInside)
}
#objc func onSignIn() {
let viewController = SignInController()
viewController.modalPresentationStyle = .fullScreen
}
}
You can present from top controller.. using this extension you will get top controller ...
extension UIApplication {
class func getTopMostViewController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
} else {
return nil
}
}
}
Then in your collection view extension
extension UICollectionView {
func setEmptyView() {
let signUpButton = UIButton(type: .custom)
signUpButton.addTarget(self, action: #selector(onSignIn), for: .touchUpInside)
}
#objc func onSignIn() {
let viewController = SignInController()
viewController.modalPresentationStyle = .fullScreen
UIApplication.getTopMostViewController()?.present(viewController, animated: true, completion: nil)
}
}

Present Modally UIViewController From UITabBarController Programmatically

Hi everyone I need to present a View Controller modally when the user selects the index 1 of my Tab bar.
I created a UITabBarController class where I instantiate all the view controllers to be shown with the tabBar
In this part of the code I manage the modal presentation of the view controller for the index 1 of the tabBar
The problem is that when I select index 1 the VCIndex1 controller is called twice ... once for the normal display of the tabBar and another time for the modal presentation
How can I present VCIndex1 modally without the tab bar calling the controller x2 times?
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
tabBar.barTintColor = UIService.Color.backgroundColor
tabBar.isTranslucent = false
tabBar.tintColor = UIService.Color.primaryColor
tabBar.selectedItem?.badgeColor = UIService.Color.secondaryColor
tabBar.unselectedItemTintColor = UIService.Color.tertiaryColor
tabBar.shadowImage = UIImage()
let vcIndex0 = UINavigationController(rootViewController: VC0())
vcIndex0 = UIImage(systemName: "rosette")
let vcIndex1 = UINavigationController(rootViewController: VC1())
vcIndex1 = UIImage(systemName: "plus.square.on.square")
let vcIndex2 = UINavigationController(rootViewController: VC2())
vcIndex2 = UIImage(systemName: "tag")
viewControllers = [vcIndex0, vcIndex1, vcIndex2]
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let indexOfTab = tabBar.items?.firstIndex(of: item)
if indexOfTab == 1 {
let vc = VC1()
vc = .fullScreen
present(vc, animated: true, completion: nil)
}
}
}
You probably want to implement shouldSelect (Apple Docs) and handle your tab-detection and modal presentation there.
Give this a try:
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
tabBar.barTintColor = .lightGray // UIService.Color.backgroundColor
tabBar.isTranslucent = false
tabBar.tintColor = .green // UIService.Color.primaryColor
tabBar.selectedItem?.badgeColor = .blue // UIService.Color.secondaryColor
tabBar.unselectedItemTintColor = .cyan // UIService.Color.tertiaryColor
tabBar.shadowImage = UIImage()
let vcIndex0 = UINavigationController(rootViewController: VC0())
vcIndex0.tabBarItem = UITabBarItem(title: "0", image: UIImage(systemName: "rosette"), tag: 0)
// just create a plain UIViewController here (it will never be seen)
//let vcIndex1 = UINavigationController(rootViewController: VC1())
let vcIndex1 = UIViewController()
vcIndex1.tabBarItem = UITabBarItem(title: "1", image: UIImage(systemName: "plus.square.on.square"), tag: 0)
let vcIndex2 = UINavigationController(rootViewController: VC2())
vcIndex2.tabBarItem = UITabBarItem(title: "2", image: UIImage(systemName: "tag"), tag: 0)
viewControllers = [vcIndex0, vcIndex1, vcIndex2]
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController == tabBarController.viewControllers?[1] {
let vc1 = VC1()
vc1.modalPresentationStyle = .fullScreen
present(vc1, animated: true, completion: nil)
return false
}
return true
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
// if you want to do something based on selected tab
if let indexOfTab = tabBar.items?.firstIndex(of: item) {
print("didSelect:", indexOfTab)
}
}
}
class VC0: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
class VC1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissMe))
view.addGestureRecognizer(tap)
}
#objc func dismissMe() -> Void {
dismiss(animated: true, completion: nil)
}
}
class VC2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
}

Tab Bar Item won't show

Currently I have 1 tab view controller, with 3 menus:
But when running, the bottom menu / image are like being cutted or didn't show perfectly like this:
It should have 3 menus.
Code:
import UIKit
class TabBarReimbursementViewController: UITabBarController{
var loadTable: Bool = false
override func viewWillAppear(animated: Bool) {
self.viewControllers![selectedIndex].viewWillAppear(true)
}
override func viewDidLoad() {
super.viewDidLoad()
// self.tabBar.barTintColor = UIColor.redColor()
self.tabBar.tintColor = UIColor.blackColor()
let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(TabBarReimbursementViewController.buttonClicked(_:)))
navigationItem.rightBarButtonItem = addButton
// UITabBarItem.appearance().setTitleTextAttributes([NSForegroundColorAttributeName: UIColor.whiteColor() ], forState: .Normal)
UITabBarItem.appearance().setTitleTextAttributes([NSForegroundColorAttributeName: UIColor.blackColor() ], forState: .Selected)
let historySelected: UIImage! = UIImage(named: "history2.png")?.imageWithRenderingMode(.AlwaysOriginal)
let approvalSelected: UIImage! = UIImage(named: "approve2.png")?.imageWithRenderingMode(.AlwaysOriginal)
let listSelected: UIImage! = UIImage(named: "listlist2.png")?.imageWithRenderingMode(.AlwaysOriginal)
(tabBar.items![1] ).selectedImage = historySelected
(tabBar.items![0] ).selectedImage = approvalSelected
(tabBar.items![2] ).selectedImage = listSelected
self.selectedViewController = self.viewControllers![1]
self.findHamburguerViewController()?.gestureEnabled = false
}
func buttonClicked(sender: UIBarButtonItem) {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewControllerWithIdentifier("MyRequestForm") as! myRequestForm
nextViewController.formType = "New"
self.navigationController!.pushViewController(nextViewController, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You CAPSPageMenu Library in which you can use tab bar controller very easily CAPSPageMenu
just add below code in Library
_menuScrollView.translatesAutoresizingMaskIntoConstraints = NO;
_menuScrollView.frame = CGRectMake(0.0, self.view.frame.size.width- _menuHeight,self.view.frame.size.width, _menuHeight);
The thing is that what you are doing is not how a tab bar controller's tab bar items work. Each child view controller (there are three of them in your storyboard screen shot) has its own tabBarItem, and that is how the tab bar gets populated.

UINavigationBar change colors on push

I'm using 2 different bar tint colors at UINavigationBar in different views. I'n changing color with that method in both views:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barTintColor = COLOR
}
When I tap on back button color is not changed smoothly (you can see blink on last second).
But everything is okay if just swipe view back instead of tapping on back button.
How to make smooth transition in both situations?
To achieve this kind of animation you should use UIViewControllerTransitionCoordinator as Apple documentation say it is :
An object that adopts the UIViewControllerTransitionCoordinator protocol provides support for animations associated with a view controller transition.(...)
So every UIViewController has own transitionController. To get this you should call in the UIViewControllerClass :
self.transitionCoordinator()
From documentation:
Returns the active transition coordinator object.
So to get the result that you want you should implement animateAlongsideTransition method in viewController transitionCoordinatior. Animation works when you click backButton and swipe to back.
Example :
First Controller :
class ViewControllerA: UIViewController {
override func loadView() {
super.loadView()
title = "A"
view.backgroundColor = .white
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "NEXT", style: .plain, target: self, action: #selector(self.showController))
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors() {
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .red
}
}
Second Controller:
class ViewControllerB : UIViewController {
override func loadView() {
super.loadView()
title = "B"
view.backgroundColor = .white
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
override func willMove(toParentViewController parent: UIViewController?) { // tricky part in iOS 10
navigationController?.navigationBar.barTintColor = .red //previous color
super.willMove(toParentViewController: parent)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.navigationBar.barTintColor = .blue
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors(){
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .blue
}
}
UPDATE iOS 10
In the iOS 10 the tricky part is to add the willMoveTo(parentViewController parent: UIViewController?) in the second ViewController. And set the navigationBar tintColor to the color value of previous controller. Also, in viewDidAppear method in second ViewControler set the navigationBar.tintColor to the color from second viewController.
Check out my example project on github
I've coded final solution that looks most comfortable to use (don't need to use a lot of overrides in own view controllers). It works perfectly at iOS 10 and easy adoptable for own purposes.
GitHub
You can check GitHub Gist for full class code and more detailed guide, I won't post full code here because Stackoverflow is not intended for storing a lot of code.
Usage
Download Swift file for GitHub. To make it work just use ColorableNavigationController instead of UINavigationController and adopt needed child view controllers to NavigationBarColorable protocol.
Example:
class ViewControllerA: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.blue }
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Push", style: .plain, target: self, action: #selector(self.showController))
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
}
class ViewControllerB: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.red }
}
let navigationController = ColorableNavigationController(rootViewController: ViewControllerA())
This worked for me:
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
navigationController?.navigationBar.barTintColor = previous view controller's navigation bar color
}
I am just wondering. For the same purpose I use UINavigationControllerDelegate. In navigationController(_:willShow:) I start the animation using transitionCoordinator?.animate(alongsideTransition:completion:). It works great when pushing new controllers, however pop doesn't.
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let dst = viewController as! ViewController
guard animated else {
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
return
}
navigationController.transitionCoordinator?.animate(alongsideTransition: { context in
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
}, completion: { context in
if context.isCancelled {
let source = context.viewController(forKey: UITransitionContextViewControllerKey.from) as! ViewController
navigationController.navigationBar.barTintColor = source.navigationBarColor
navigationController.navigationBar.tintColor = source.tintColor
navigationController.navigationBar.barStyle = source.barStyle
}
})
Do you see any reason why it should work with pushes but not pops?

Resources