Current problems and things I want to know
UIBarButtonItem action method does not work in certain cases. I want to know why it just doesn't work.
Case1
case1 storyboard
Just added NavigationController to the default ViewController
import UIKit
class ViewController: UIViewController {
let okButton = UIBarButtonItem(title: "OK", style: .plain, target: self, action: #selector(okButtonTapped(_:)))
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = okButton
}
#objc func okButtonTapped(_ sender: UIBarButtonItem) {
print("OK!")
}
}
In this case, the OK button does not work.
There is no error message.
Case2
case2 storyboard
Added Navigation Bar to default ViewController.
UINavigationItem connects to the outlet with the name myNavigationItem.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myNavigationItem: UINavigationItem!
let okButton = UIBarButtonItem(title: "OK", style: .plain, target: self, action: #selector(okButtonTapped(_:)))
override func viewDidLoad() {
super.viewDidLoad()
myNavigationItem.rightBarButtonItem = okButton
}
#objc func okButtonTapped(_ sender: UIBarButtonItem) {
print("OK!")
}
}
In this case, the OK button work.
Summary
I understand that even in case 1, if okButton initialization is executed in viewDidLoad, it will work.
I want to know why case2 works but case1 doesn't work.
Thnak you.
If you initialise the okButton before viewDidLoad you can't set the target self. So initialise the button in viewDidLoad
class ViewController: UIViewController {
var okButton:UIBarButtonItem?
override func viewDidLoad() {
super.viewDidLoad()
okButton = UIBarButtonItem(title: "OK", style: .plain, target: self, action: #selector(okButtonTapped(_:)))
self.navigationItem.rightBarButtonItem = okButton
}
#objc func okButtonTapped(_ sender: UIBarButtonItem) {
print("OK!")
}
}
If you need the button to be accessible for the entire class, just declare it as you did, but initialise it in viewDidLoad.
Case 2 works because the storyboard gives you the myNavigationItem when view did load is called. Check the value of self.navigationItem in Case 1 in viewDidLoad.
I summarized it because I solved it myself.
During initialization of instance properties, self cannot be used because the instance itself has not yet been created. However, it is treated as nil without causing an error. This is true for both cases.
If target is set to nil, it will be executed when an action method (ngButtonTapped) is found going up the View hierarchy.
In case1, there is no ViewController above the navigation bar, so no action method is found and nothing happens. (The VIew hierarchy can be confirmed with the Xcode view debugger)
In Case2, the action method (ngButtonTapped) is executed because ViewController is located above the navigation bar.
Related
I added an extension to UIViewController to add a close button
extension UIViewController {
func addCloseButton() {
let button = UIBarButtonItem(image: #imageLiteral(resourceName: "bar_close"),
landscapeImagePhone: nil,
style: .done,
target: self,
action: #selector(UIViewController.dismiss(animated:completion:)))
navigationItem.leftBarButtonItem = button
}
}
When i tap the barbutton i get a crash directly to AppDelegate.
Any hints? Seems related to the selector.
You can't use dismiss(animated:completion:) as selector here because it takes two arguments bool and closure and bar button action pass args as UIBarButtonItem which cause app crash.
so change your code like this.
extension UIViewController {
func addCloseButton() {
let button = UIBarButtonItem(image: #imageLiteral(resourceName: "rightgreen"),
landscapeImagePhone: nil,
style: .done,
target: self,
action: #selector(onClose))
navigationItem.leftBarButtonItem = button
}
#objc func onClose(){
self.dismiss(animated: true, completion: nil)
}
}
However this question has accepted answer which load extra one method addCloseButton in each and every viewcontroller still posting a answer will going to help someone
NOTE : This example for adding barbutton item automatically and also handle action for pop view controller.
As Protocol extension doesn't provide a to implement selector methods so to get the rid of it I have created this solution.
First thing you need is BaseVC which is subclass of UIViewController and all of your view controller going to be inherited by BaseVC like your class LoginVC:BaseVC ...
now declare protocol
protocol PopableClass {
func popSelf (animated:Bool)
}
extension PopableClass where Self : UIViewController {
func popSelf (animated:Bool) {
self.navigationController?.popViewController(animated: animated)
}
}
In your Base VC add two methods and call setupNavigationBar from viewDidLoad
func setupNavigationBar () {
if self is PopableClass {
let barbuttonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "back"), landscapeImagePhone: #imageLiteral(resourceName: "back"), style: .plain, target: self, action: #selector(popViewController))
self.navigationItem.leftBarButtonItem = barbuttonItem
}
}
//--------------------------------------------------------------------------------
#objc func popViewController () {
if self is PopableClass {
(self as! PopableClass).popSelf(animated: true)
}
}
You did it !!
Now in whatever class you need back button to pop view controller just use like this
class PushedClass: BaseVC,PopableClass
Hope it is helpful
How do I set a custom image to all back buttons of view controllers pushed in a UINavigationController?
My issues are:
must look like leftBarButtonItem, position-wise (because the backBarButtonItem itself is too glued to the left and I can't seem to change it's horizontal alignment).
has to be on all back actions (instead of manually setting on each view controller).
having a method setCustomBackButton and calling it on each view controller is also not an option, I'm looking for something like UINavigationBar.appearance(), i.e., throughout the app.
Something like this:
But with the back action working without me manually setting the selector on each view controller.
UPDATE: In response to Joe's solution, I'm getting that error:
UINavigationBar.appearance().backIndicatorImage = UIImage(named: "backArrow")
See Here: https://www.raywenderlich.com/108766/uiappearance-tutorial
Below answer based on the following OP answers:
Custom Back Button With Image and How to remove all navigationbar back button title
Try below code in didFinishLaunchingWithOptions method in AppDelegate.
To setting up a custom back button:
let backArrowImage = UIImage(named: "back") // set your back button image here
let renderedImage = backArrowImage?.withRenderingMode(.alwaysOriginal)
UINavigationBar.appearance().backIndicatorImage = renderedImage
UINavigationBar.appearance().backIndicatorTransitionMaskImage = renderedImage
To hide a back button title:
let barAppearace = UIBarButtonItem.appearance()
barAppearace.setBackButtonTitlePositionAdjustment(UIOffsetMake(0, -60), for:UIBarMetrics.default)
Output: Updated
Update:
You need to add the following code to your More Information viewController to keep the title position.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
You can create your own subclass of UINavigationController and change the button inside the navigationController(_:willShow:animated:) delegate method as follows:
class MyNavigationController: UINavigationController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
interactivePopGestureRecognizer?.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if viewController != self.viewControllers.first { // don't add button to rootViewController
let backButton = UIBarButtonItem(image: UIImage(named: "backArrow"), style: .plain, target: self, action: #selector(popViewController(animated:)) )
viewController.navigationItem.leftBarButtonItem = backButton
}
}
}
Theoretically the above delegate method could live anywhere, but this way its logical and easy to select where you want to have this functionality.
Also don't forget to set the interactivePopGestureRecognizer delegate for not loosing the edge swipe gesture to go back (this somehow breaks when setting a new leftBarButtonItem).
The above method could be further improved by keeping track of which view controllers were already shown and then only replace the leftBarButtonItem on new ones (right now it also replaces it when going back/popping to an already shown view controller).
Try this Swift 4.2
extension YouFirstViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if !(viewController is YouFirstViewController) {
let backButton = UIBarButtonItem(image: UIImage(named: "icnBack"), style: .plain, target: self, action: #selector(popview))
viewController.navigationItem.leftBarButtonItem = backButton
}
}
#objc func popview() {
navigationController?.popViewController(animated: true)
}
}
onYouFirstViewController
class YouFirstViewController: UIViewcontroller {
override func viewDidLoad() {
self.navigationController?.delegate = self
}
}
I have a Navigation controller and I'm trying to put a button on the right of navigation bar but I can't handle the tap action. I'm declaring the UIBarButtonItem like this
let navigationButton = UIBarButtonItem.init(title: "Logout", style: .done, target: self, action: #selector(RestaurantsListViewController.logoutAction))
I'm adding the button on the viewDidLoad func
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = navigationButton
}
and the function that I'm trying to use to handle the tap event is this
func logoutAction(sender: AnyObject?){
print("Logout")
}
but when I press the button, the message is not printed in console.
Try this:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "ButtonName", style: .done, target: self, action: #selector(YourViewController.yourAction))
}
let okbtn = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.plain, target: self, action: #selector(ViewController.logoutAction))
Try This
otherwise You need to replace func like
func logoutAction()
{
print("logout")
}
The issue here is when you create navigationButton as a class property, it gets initialized before self is initialized. So self doesn't exist yet when you pass it in as the target of the button.
There are a couple ways to fix it, including the answer by #Mannopson where you initialize the button inside the viewDidLoad, which ensures that self has already been created.
Another way to solve this issue is to declare navigationButton to be a lazy var:
lazy var navigationButton = UIBarButtonItem.init(title: "Logout", style: .done, target: self, action: #selector(RestaurantsListViewController.logoutAction))
The lazy var ensures that the property only gets initialized when the property gets accessed (hence it being a lazy initialization). Since the first time it is accessed happens in viewDidLoad, we can also be sure that self has been created. Hope this gives you more context!
How can i create one navigation menu for whole project.
I created one but for specific view only using SWRevealViewController.
You cannot do it in two ways.
a) Create a ViewController with side menu and subclass the view controller every time you need a side menu.
class BaseViewController: UIViewController {
override func viewDidLoad() {
let menuButton = UIBarButtonItem.init(image: image, style: .plain, target: self, action: #selector(funcToCall))
navigationItem.leftBarButtonItem = menuButton
}
}
and subclass BaseViewController
class MyViewController: BaseViewController {
}
b) Another method and the better one is using extension
extension UIViewController {
func addMenu() {
let menuButton = UIBarButtonItem.init(image: image, style: .plain, target: self, action: #selector(SWRevealViewController.rightRevealToggle(_:)))
navigationItem.leftBarButtonItem = menuButton
}
}
and call addMenu() in MyViewController
class MyViewController: UIViewController {
override func viewDidLoad() {
addMenu()
}
}
One of the many ways is to create a swift file Helper.swift
then
import UIKit
extension UIViewController {
func addMenu() {
let menuButton = UIBarButtonItem.init(image: image, style: .plain, target: self, action: #selector(SWRevealViewController.rightRevealToggle(_:)))
navigationItem.leftBarButtonItem = menuButton
}
}
You can embed your RevealVC with navigation controller and add navigation item to RevealVC, but such a menu will be static.
I usually use TopViewController class:
class TopViewController: UIViewController {
#IBOutlet weak var menuButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
if self.revealViewController() != nil {
revealViewController().rightViewRevealWidth = view.frame.size.width / 2
menuButton.target = revealViewController()
menuButton.action = #selector(SWRevealViewController.rightRevealToggle(_:))
view.addGestureRecognizer(revealViewController().panGestureRecognizer())
}
}
}
Each view controller will just inherit this options. In this case you should create navigationVC in storyboard for each view controller (I think it's the best solution). Do not forget to drag outlets from storyboard to TopViewController.
Creating only one NavigationVC is impossible i suppose.
I'm trying to set in almost each page a custom back button and I'm repeating the same code in each page like this;
let buttonItem = UIBarButtonItem(image: UIImage(named: "arrow_back"), style: UIBarButtonItemStyle.Plain, target: self, action:"popBack")
buttonItem.tintColor=UIColor.blackColor()
navigationItem.leftBarButtonItem = backButtonItem()
And I think this is the wrong way for it. So please could you tell what is the best way for this?
I've done this by creating a category on UIViewController. In that file I created a method called addBackButton, where you can put your code in once, then expose the method in your .h file. Then in any of your view controller subclasses you can import your category and call [self addBackButton];
You have various options here.
1) UIViewController extension
extension UIViewController {
func brandedBackButton() {
let buttonItem = UIBarButtonItem(image: UIImage(named: "back"), style: UIBarButtonItemStyle.Plain, target: self, action:"popBack")
buttonItem.tintColor=UIColor.blackColor()
navigationItem.leftBarButtonItem = buttonItem
}
}
and then just call in your view controller
override func viewDidLoad() {
super.viewDidLoad()
brandedBackButton()
}
2) Base View Controller Class
You would basically put a class in between your controllers and the UIViewController.
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let buttonItem = UIBarButtonItem(image: UIImage(named: "back"), style: UIBarButtonItemStyle.Plain, target: self, action:"popBack")
buttonItem.tintColor=UIColor.blackColor()
navigationItem.leftBarButtonItem = buttonItem
}
}
and then you would inherit from this BaseViewController instead of standard UIViewController.
class YourWhateverViewController: BaseViewController {
//implementation here....
}
Both the category and the base class can also accommodate the popBack custom method so you would end up really with literally 10 characters to get this behaviour anywhere...
I suggest you create a custom UIButton class and reuse it every time you want it.
class backButtonItem: UIBarButtonItem {
convenience init(target: AnyObject?) {
self.init(image: UIImage(named: "arrow_back"), style: .Plain, target: target, action: "popBack")
self.tintColor = UIColor.blackColor()
}}
just type the following script when you use it:
navigationItem.leftBarButtonItem = backButtonItem(target: self)