In my app, I have a toolbar with UIBarButtonItems.
In most circumstances, the UIBarButtonItems are set via storyboard, and look as follows:
In a special case, I have to replace one UIBarButtonItem programmatically. This is done with the following code:
let rotatingButton = UIButton(type: .custom)
rotatingButton.setImage(UIImage(named: "LocalizationInUseNoFix"), for: .normal)
rotatingButton.addTarget(self, action: #selector(localizationButtonTapped), for: .touchUpInside)
rotatingButton.rotateStart()
let barButtonItem = UIBarButtonItem(customView: rotatingButton)
leftBarButtonItems![2] = barButtonItem
When the rotatingButton is displayed in the toolbar, it placed at a different position. It is shifted to the right, as you can see here:
How can I achieve to place both UIBarButtonItems at the same position?
EDIT:
By now I realized that the horizontal shift of the programmatically created UIBarButtonItem is not always the same, without any changes to the code: Sometimes it is shifted left, and not right:
EDIT 2:
I found a workaround:
If I set a width constrain to my button like
rotatingButton.widthAnchor.constraint(equalToConstant: 40).isActive = true
then the button is apparently always correctly placed. But I hate to hard-code constraints like this.
Is there a more elegant way to do it?
Try the below steps to perform your task:
Store left bar button items into an NSMutableArray
Replace desired UIBarbuttonItem
Set leftbarbuttonitems to this new array
Hope this steps will work
When you set the image on UIBarButton programmatically, the contentmode of the leftBarButtonItems becomes 'left' and rightBarButtonItems become 'right'. But from storyboard, it is centered. Set the image and adjust the contentMode as required.
All are working fine for Navigationbar and Toolbar
class ViewController: UIViewController {
#IBOutlet weak var toolbar: UIToolbar!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func leftAction(_ sender: Any) {
}
#IBAction func rightAction(_ sender: Any) {
}
#IBAction func changeLeftItems(_ sender: Any) {
if let items = self.navigationItem.leftBarButtonItems {
var addItems = [UIBarButtonItem]()
addItems.append(contentsOf: items)
let barItem = UIBarButtonItem(title: "3", style: UIBarButtonItemStyle.plain, target: self, action: #selector(ViewController.leftAction(_:)))
addItems.append(barItem)
self.navigationItem.leftBarButtonItems = addItems
}
if let items = self.toolbar.items {
var addItems = [UIBarButtonItem]()
addItems.append(contentsOf: items)
let barItem = UIBarButtonItem(title: "L3", style: UIBarButtonItemStyle.plain, target: self, action: #selector(ViewController.leftAction(_:)))
addItems.insert(barItem, at: 2)
self.toolbar.setItems(addItems, animated: true)
}
}
}
This is the best solution I found so far:
Get the width of a view of another bar button item using key value coding. This is from Jeremy W. Sherman’s answer here.
Please note that it does not use any private API, see the discussion there. The worst thing that can happen is that the view property of the UIBarButtonItem cannot be accessed. In this case, I use a default value:
var leftBarButtonItems = self.navigationItem.leftBarButtonItems
let rotatingButton = UIButton(type: .custom)
rotatingButton.setImage(UIImage(named: "LocalizationInUseNoFix"), for: .normal)
rotatingButton.addTarget(self, action: #selector(localizationButtonTapped), for: .touchUpInside)
rotatingButton.rotateStart()
// Get the width of the bar button items if possible, else set default
let leftmostBarButtonItem = leftBarButtonItems![0]
let barButtonItemWidth: CGFloat
if let leftmostBarButtonItemView = leftmostBarButtonItem.value(forKey: "view") as? UIView {
barButtonItemWidth = leftmostBarButtonItemView.frame.size.width
} else {
barButtonItemWidth = 40.0
}
rotatingButton.widthAnchor.constraint(equalToConstant: barButtonItemWidth).isActive = true
let barButtonItem = UIBarButtonItem(customView: rotatingButton)
leftBarButtonItems![2] = barButtonItem
self.navigationItem.leftBarButtonItems = leftBarButtonItems
This is working fine for me. Best way is identify item to replace and change the content
#IBAction func changeLeftItems(_ sender: Any) {
if let items = self.toolbar.items {
var addItems = [UIBarButtonItem]()
addItems.append(contentsOf: items)
let barItem = UIBarButtonItem(title: "L5", style: UIBarButtonItemStyle.plain, target: self, action: #selector(ViewController.leftAction(_:)))
addItems.remove(at: 1)
addItems.insert(barItem, at: 1)
self.toolbar.setItems(addItems, animated: true)
}
}
Related
I know this question has been asked before but nothing worked for me and I had to ask it again.
I want an image as my back button in navigation bar, just want to change the appearance of the back button. I don't want to add a button and add selectors for it.
I tried the following code:
let backImage = UIImage(named: "Back_button")
let backAppearance = UIBarButtonItem.appearance()
backAppearance.setBackButtonBackgroundImage(backImage, for: .normal, barMetrics: .default)
navigationController?.navigationBar.backIndicatorTransitionMaskImage = backImage
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItem.Style.plain, target: nil, action: nil)
I also tried setting the back image and back mask using storyboard but both these approaches place a black circle on my back image.
I tried setting another image as back mask by setting its alpha content equal to zero using the code but it didn't work either.
please help.
let backButton = UIBarButtonItem()
backButton.title = "Back"
backButton.image = UIImage(named: "Back_button")
self.navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
You can do this to customize your Back button. And you don't have to worry about adding selectors.
This code works with Swift 5.
let backButton: UIButton = UIButton()
backButton.setImage(UIImage(named: "back"), for: UIControl.State())
backButton.addTarget(self, action:#selector(SearchResultsViewController.onBack), for: UIControl.Event.touchUpInside)
let leftBarButtonItem = UIBarButtonItem(customView: backButton)
navigationItem.leftBarButtonItem = leftBarButtonItem
I used this code to customize the back button on only one of my views:
self.navigationController?.navigationBar.topItem?.backButtonTitle = ""
let backButton = UIBarButtonItem(image: UIImage(named: "back"), style: .plain, target: self, action: #selector(goBack))
navigationItem.leftBarButtonItem = backButton
#objc func goBack() {
self.navigationController?.popViewController(animated: true)
}
Create a custom class for define navigation bar traits
Create an extension to UINavigationController for configure it
import UIKit
private final class MyNavigationBarTraits {
public var backIndicatorImage: UIImage?
public var backIndicatorTransitionMaskImage: UIImage?
public func apply(to navigationBar: UINavigationBar) {
navigationBar.backIndicatorImage = backIndicatorImage
navigationBar.backIndicatorTransitionMaskImage = backIndicatorTransitionMaskImage
}
public init(navigationBar: UINavigationBar) {
backIndicatorImage = navigationBar.backIndicatorImage
backIndicatorTransitionMaskImage = navigationBar.backIndicatorTransitionMaskImage
}
}
public typealias Callback<T> = (_: T) -> Void
public extension UINavigationController {
private struct AssociationKeys {
static var navigationBarTraits = "ws_nc_navigationBarTraits"
}
private var navigationBarTraits: MyNavigationBarTraits? {
get {
return objc_getAssociatedObject(self, &AssociationKeys.navigationBarTraits) as? MyNavigationBarTraits
}
set {
objc_setAssociatedObject(self, &AssociationKeys.navigationBarTraits, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func configureBar(block: Callback<UINavigationBar>) {
navigationBarTraits = MyNavigationBarTraits(navigationBar: navigationBar)
block(navigationBar)
}
func resetBar() {
navigationBarTraits?.apply(to: navigationBar)
navigationBarTraits = .none
}
}
And then you can configure your navigation bar in your ViewController's viewWillAppear (for example tintColor)
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.configureBar { navigationBar in
// You can customize your navigation bar in here!
navigationBar.tintColor = .red
}
}
If you want to use this customization just in one View Controller you should reset bar in your View Controller's viewWillDisappear
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.resetBar()
}
Simply Add Below Methods in Your ViewController :
func setLeftBarBackItem() {
let leftBarBackItem = UIBarButtonItem(image: #imageLiteral(resourceName: "imgBack"), style: .plain, target: self, action: #selector(self.clickToBtnBackItem(_:)))
self.navigationItem.leftBarButtonItem = leftBarBackItem
}
func clickToBtnBackItem(_ sender: UIBarButtonItem) {
view.endEditing(true)
_ = navigationController?.popViewController(animated: true)
}
func setTranspertNavigation()
{
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = true
self.navigationController?.view.backgroundColor = .clear
}
Inside Your ViewController's ViewDidLoad Method, Set backButton As :
self.navigationController?.isNavigationBarHidden = false
AppDelegate.shared().setupNavigationBar()
setLeftBarBackItem()
setTranspertNavigation()
self.title = "Title Here"
first post so apologies if I mess something up. I have researched this for hours upon hours and read other posts here on stack exchange to no avail.
I have created a nib file that defines a custom view and have defined a custom class (UIView) to manage the outlets of the custom view. As you can see from the code below excerpted from my custom UIView class associated with the nib, I have a date picker as the input view for the custom class and a UIToolBar with two UIBarButtonItems. Both of these appear as desired through a tap gesture recognizer... however the problem is the UIBarButtonItems do not call the action when tapped. Placing a breakpoint in the action function reveals that the code is never run. I feel that something with the view lifecycle is preventing a reference from being made, but I am new to Swift so some help here would be appreciated. I don't think it is selector syntax as the tap gesture recognizer works as desired. I've tried messing with button click handling access levels. I've tried doing input view setup when the view awakes from the nib as well, along with trying to put the code in different parts of the lifecycle.
If it matters for lifecycle's sake, this nib is a part of a table view cell. I call for this nib to be loaded when the table view cell awakes from it's nib.
Thanks!
#IBOutlet weak var timerStackView: UIStackView!{
didSet{
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(HandleTap(_:)))
timerStackView.addGestureRecognizer(tapGesture)
}
}
var datePicker: UIDatePicker {
let picker = UIDatePicker()
picker.backgroundColor = UIColor.black
picker.isOpaque = false
picker.setValue(UIColor.white, forKey: "textColor")
return picker
}
var datePickerAccessoryView: UIToolbar {
let accessoryView = UIToolbar()
let doneButton = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(handleDatePickerButtonClick(_:)))
doneButton.tintColor = UIColor.white
let cancelButton = UIBarButtonItem(title: "Cancel", style: UIBarButtonItemStyle.plain, target: self, action: #selector(handleDatePickerButtonClick(_:)))
cancelButton.tintColor = UIColor.white
accessoryView.setItems([cancelButton, doneButton], animated: true)
return accessoryView
}
override var inputView: UIView? {return datePicker}
override var inputAccessoryView: UIView? {return datePickerAccessoryView}
override var canBecomeFirstResponder: Bool {return true}
override var canResignFirstResponder: Bool {return true}
// MARK: - Private functions
#objc fileprivate func HandleTap(_ sender: UITapGestureRecognizer) -> Void {
if !self.isFirstResponder {
switch sender.state {
case .ended:
datePicker.date = Date()
self.becomeFirstResponder()
default:
break
}
}
}
#objc #IBAction internal func handleDatePickerButtonClick(_ sender: UIBarButtonItem) -> Void {
switch sender.title! {
case "Done":
// To be implemented
case "Cancel":
// To be implemented
default:
break
}
}
You are initialising the UIToolbar without a frame and that would make it not register any touch events because they would be out of the toolbar's bounds.
Replace let accessoryView = UIToolbar() with something like let accessoryView = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 44))
Or you can call accessoryView.sizeToFit() before return accessoryView
I suppose the tap gesture recognizer is interfering with native UIBarButtonItem click events. But why do you use a gesture recognizer for that?
You should better add an action to each particular UIBarButtonItem.
I have written an extension on UIButton which converts a UIButton into a UIBarButtonItem.
I tried to most simple solution using init(customView on UIBarButtonItem passing in my button, like this:
lazy var myButton: UIButton = {
let button = UIButton(type: .custom)
// setting up button here...
return button
}()
let barButtonItem = UIBarButtonItem(customView: myButton)
But I had several constraints issues. Actually I did not only want to put one button, but three items in the navigation bar. So I tried putting my three buttons into a UIStackView and then set navigationItem.rightBarButtonItem = UIBarButtonItem(customView: stackView) which works like a charm on iOS 10. But on iOS 9 the position of the buttons did not work.
Anyway I resorted to using UIBarButtonItem instead of UIButtons, but I did not want to create UIBarButtons for iOS 9 but UIButtons for iOS 10. So I wrote an extension creating UIBarButtonItems from UIButtons. Which enabled this code.
Solution handling iOS 9 and iOS 10
func setupNavigationBar() {
if #available(iOS 10.0, *) {
let stackView: UIStackView = [.views(buttons)] //syntax enabled by framework `ViewComposer`: github.com/Sajjon/ViewComposer
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: stackView)
} else {
navigationItem.rightBarButtonItems = buttons.reversed().flatMap { $0.barButtonItem } // syntax enabled by extension below
}
}
Here is my code for my extension.
Extension UIButton
extension UIButton {
var barButtonItem: UIBarButtonItem? {
return barButtonItem()
}
func barButtonItem(
style: UIBarButtonItemStyle = .plain,
state: UIControlState = .normal,
controlEvent: UIControlEvents = .primaryActionTriggered
) -> UIBarButtonItem? {
guard
let target = allTargets.first,
let selectorName = actions(forTarget: target, forControlEvent: controlEvent)?.first,
case let image = image(for: state), case let title = title(for: state),
(image != nil || title != nil)
else { return nil }
let action = NSSelectorFromString(selectorName)
if let image = image {
return UIBarButtonItem(image: image, style: style, target: target, action: action)
}
if let title = title {
return UIBarButtonItem(title: title, style: style, target: target, action: action)
}
return nil //should not happen
}
}
Question 1: Why did positioning the UIButtons inside the UIStackView not work on iOS 9?
Question 2: Is my extension unnecessarily complex? Or insecure somehow?
The extension can be simplified
extension UIButton {
func toBarButtonItem() -> UIBarButtonItem? {
return UIBarButtonItem(customView: self)
}
}
I currently am working on an app in Swift where in my viewDidLoad() method I have purposely hidden my rightBarButton on my navigation bar like this:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: #selector(TableViewController.dismiss))
self.navigationItem.setRightBarButtonItem(nil, animated: true)
However, under certain circumstances, I would like to display the rightBarButton. How would I do this? What would be the opposite of the above line of code?
Once you set the bar button item to nil, it is gone. Something you can do however, is store the bar button item like so:
let barButtonItem = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: #selector(TableViewController.dismiss));
and then you can make it appear/disappear like so:
self.navigationItem.rightBarButtonItem = barButtonItem
self.navigationItem.setRightBarButtonItem(nil, animated: true)
then just access the barButtonItem whenever you want it to appear/disappear.
You can do one of the following two options:
Keep a reference of your UIBarButtonItem and every time you disappear you save it to then when you want to show it again you set the old value.
Play with the color of the UIBarButtonItem and the enabled/disable property to enable the interaction with it.
The first choice always keep a reference globally to the UIBarButtonItem and the second need to know the exact color of the original UIBarButtonItem to give to its original state:
First Option:
private var isHidden: Bool!
private var righBarButtonItem: UIBarButtonItem!
#IBAction func hideButton(sender: AnyObject) {
if self.isHidden == true {
self.isHidden = false
self.navigationItem.rightBarButtonItem = righBarButtonItem
}
else {
self.isHidden = true
righBarButtonItem = self.navigationItem.rightBarButtonItem
self.navigationItem.setRightBarButtonItem(nil, animated: true)
}
}
Second Option:
#IBAction func hideButton(sender: AnyObject) {
if self.isHidden == true {
self.isHidden = false
self.navigationItem.rightBarButtonItem?.tintColor = UIColor.clearColor()
self.navigationItem.rightBarButtonItem?.enabled = false
}
else {
self.isHidden = true
self.navigationItem.rightBarButtonItem?.tintColor = UIColor.blueColor()
self.navigationItem.rightBarButtonItem?.enabled = true
}
}
In the above examples I set a variable with the state of the UIBarButtonItem for purposes of know the value and and #IBOutlet to hide/show the UIBarButtonItem. The variable isHidden need to set it's initial value in the viewDidLoad.
I hope this help you.
There is a Save (System Item) on my navigation bar as BarButtonItem I am showing UIActivityIndicatorView on the navigation bar when user clicks this Save Button and I want to appear this Barbutton(Save) again on certain condition. First I think the problem is I am adding a indicator on customView so I don't need to hide the barbutton.It automatically hides itself after I start the indicator. But don't know now how to show Save Button again. or how can I remove the indicator from customView
This is how I am doing
#IBOutlet weak var saveButtonOutlet: UIBarButtonItem!
var activityIndicatorView:UIActivityIndicatorView!
func showActivityIndicator() {
activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.White)
activityIndicatorView.frame = CGRectMake(0, 0, 14, 14)
activityIndicatorView.color = UIColor().blueColorIOS()
activityIndicatorView.startAnimating()
let barButtonItem = UIBarButtonItem(customView: activityIndicatorView)
self.navigationItem.rightBarButtonItem = barButtonItem
}
#IBAction func saveButtonClicked(sender: UIBarButtonItem) {
showActivityIndicator()
ServerRequest.postToServer(url, params: params){
result, error in
if let result = result {
let code = result["code"] as? Int
print(result)
if (code==200){
dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), { ()->() in
self.activityIndicatorView.hidden = true
self.activityIndicatorView.hidesWhenStopped = true
//here want to show again "saveButtonOutlet"
})
}
}
}
}
}
So one way to do this is to create the Save button again, and setting the rightBarButtonItem again:
...
self.activityIndicatorView.hidden = true
self.activityIndicatorView.hidesWhenStopped = true
let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Save, target: self, action: "saveButtonClicked:")
self.navigationItem.rightBarButtonItem = barButtonItem
And I'd also replace self.activityIndicatorView.hidden = true with self.activityIndicatorView.stopAnimating() to properly use the hidesWhenStopped property.
I think all you need to do is reset the self.navigationItem.rightBarButtonItem to the saveButtonOutlet.
Worked right now for me.