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
Related
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.
I am trying to make a custom back button using this code:
let back = UIImage(named: "header_backarrow")
let backView = UIImageView(image: back)
let backItem = UIBarButtonItem(customView: backView)
navigationItem.leftBarButtonItem = backItem
I want the navigation item to perform this code:
func dismissManual() {
dismiss(animated: true, completion: nil)
}
I have tried many things like following this Stack Overflow post: Execute action when back bar button of UINavigationController is pressed
I have also tried making it a navigationItem.backBarButtonItem; however, nothing seems to work. Some things show the correct custom image, but do not work as a button; on the other hand, some work as a button, but do not show the correct image.
Does anybody know how I can show the correct image and make the item work as a button? Thanks.
Do it as follows:
override func viewDidLoad() {
super.viewDidLoad()
let back = UIImage(named: "header_backarrow")
let backView = UIImageView(image: back)
backView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissManual))
backView.addGestureRecognizer(tap)
let backItem = UIBarButtonItem(customView: backView)
navigationItem.leftBarButtonItem = backItem
}
#objc func dismissManual() {
print("print----")
// dismiss(animated: true, completion: nil)
}
Add gesture to backView it will work!
It's similiar to this question IOS - Swift - adding target and action to BarButtonItem's customView
Swift 4.1
The issue is that UIImage does not have tap recognition. You will have to add a tap gesture recognizer to your backView.
lazy var singleTap: UITapGestureRecognizer = {
let singleTap = UITapGestureRecognizer(target: self, action: #selector(tapDetected))
singleTap.numberOfTapsRequired = 1
return singleTap
}()
// Actions
#objc func tapDetected() {
dismiss(animated: true, completion: nil)
}
If you show your code, with some screenshots I can give more help if this doesn't solve the issue.
You are creating your UIBarButtonItem incorrectly. You do not need the image view.
Do it as follows:
let back = UIImage(named: "header_backarrow")
let backItem = UIBarButtonItem(image: back, style: .plain, target: self, action: #selector(dismissManual))
navigationItem.leftBarButtonItem = backItem
#objc func dismissManual() {
dismiss(animated: true, completion: nil)
}
Note that the function must be marked with #objc.
Depending on your image and how you want it displayed, you may need to create the image as follows:
let back = UIImage(named: "header_backarrow").withRenderingMode(.alwaysOriginal)
Another option is to create a UIButton with the image and setup to call your dismissManual function. Create the UIBarButtonItem with the button as the custom view.
But it's easier to create a standard UIBarButtonItem when all you have is a simple image or a simple string.
let barButtonItem = UIBarButtonItem(image: UIImage(named: "backImgs"),
style: .plain,
target: self,
action: #selector(menuButtonTapped))
// Adding button to navigation bar (rightBarButtonItem or leftBarButtonItem)
self.navigationItem.rightBarButtonItem = barButtonItem
// Private action
#objc fileprivate func menuButtonTapped() { // body method here }
Check out this, it may help Thanks.
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 following helper class
class PDFPreviewHelper {
var pdfNavigationController: UINavigationController!
func previewButtonPressed(rootViewController: UIViewController) {
let pdfViewController = PDFViewController(resource: "final.pdf")
pdfNavigationController = UINavigationController(rootViewController: pdfViewController)
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "backButtonPressedInPDF")
pdfViewController.navigationItem.setLeftBarButtonItem(backButton, animated: false)
rootViewController.presentViewController(pdfNavigationController, animated: true, completion: nil)
}
func backButtonPressedInPDF() {
pdfNavigationController.dismissViewControllerAnimated(true, completion: nil)
}
}
I call a function in above helper class in my rootviewcontroller like following:
func previewInPdfButtonPressed() {
let a = PDFPreviewHelper()
a.viewI129InPDF(self)
}
I successfully modally present pdfNavigationController on top of my rootViewController, but whenever i press back button, nothing gets called. Why is this so? I set a break point in backButtonPressedInPDF function and it doesn't even hit the break point.
You need to assign leftBarButtonItem to UINavigationController via below way.
pdfViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "backButtonPressedInPDF")
I think the problem lies here.
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "backButtonPressedInPDF")
To:
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "backButtonPressedInPDF:")
I believe the problem as at action: "backButtonPressedInPDF" where there is a missing :
In addition, change your method by adding (sender:AnyObject) also?
func backButtonPressedInPDF(sender:AnyObject) {
pdfNavigationController.dismissViewControllerAnimated(true, completion: nil)
}
The issue is you are creating a temporary instance of PDFPreviewHelper class and using it to present the view controller. Not keeping the reference.
This can be fixed in two ways:
Method 1: Add previewInPdfButtonPressed method to your PDFViewController class. Then change implementation like following.
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: pdfViewController, action: "backButtonPressedInPDF")
Method 2:
You should keep the object a until you dismiss your PDFViewController-navigationcontroller.
Even if you do like this you will get an exception like
2016-03-16 15:43:03.140 XXXX[3757:585277] *** NSForwarding: warning: object 0x7ff5eb72be70 of class 'XXXX.PDFPreviewHelper' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[XXXX.PDFPreviewHelper backButtonPressedInPDF]
This is because you are not subclassing NSObject for your PDFPreviewHelper.
You can fix it in two ways,
Make PDFPreviewHelper as NSObject subclass.
Add dynamic
keyword to the function like dynamic func previewInPdfButtonPressed() {....}.
Resolution to this issue is well described here
func previewInPdfButtonPressed()
{
let a = PDFPreviewHelper()
a.viewI129InPDF(self)
}
The problem is a is released when the program goes out of the scope of this function. The action you target on a of course can't be executed anymore
solution add a as a property of the viewController
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)