Swift, confusion with UIBarButtonItem - ios

I began to learn Swift recently. When I tried to make my first App I got confused with UIBarButtonItem. If I put let UIBarButtonItem initialization outside the viewDidLoad() function, nothing happens when I press the Next Button.
class ViewController: UIViewController, UITextFieldDelegate {
let rightBarButton: UIBarButtonItem = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(onClickNext(button:)))
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.navigationItem.rightBarButtonItem = rightBarButton
}
func onClickNext(button: UIBarButtonItem) {
print("should push view controller")
}
}
However, when I put the initialization into the viewDidLoad() function, the output area does output the sentense that I set in the onClickNext(button:) function.
class ViewController: UIViewController, UITextFieldDelegate {
var rightBarButton: UIBarButtonItem?
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.rightBarButton = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(onClickNext(button:)))
self.navigationItem.rightBarButtonItem = rightBarButton
}
func onClickNext(button: UIBarButtonItem) {
print("should push view controller")
}
}
Also, I I found that when I put the initialization outside the viewDidLoad() function, and I add a UITextField to viewController, the rightBarButton works if I touch the textfield before I press the button.
That make me confused. What is the mechanism?

Well, maybe you are missing how a ViewController works inside.
First, viewDidLoad is the area were you usually setup or initialize any view or properties of the view. This method is also called only once during the life of the view controller object. This means that self already exists.
Knowing this, is important to understand what a let property does, (from Apple)
A constant declaration defines an immutable binding between the constant name and the value of the initializer expression; after the value of a constant is set, it cannot be changed. That said, if a constant is initialized with a class object, the object itself can change, but the binding between the constant name and the object it refers to can’t.
Even though the upper area is where you declare variables and constants, is usually meant for simple initialization, it's an area for just telling the VC that there is an object that you want to work with and will have a class global scope, but the rest of functionality will be added when the view hierarchy gets loaded (means that the object does not depends of self, for example, when adding target to a button, you are referring to a method inside of self)....this variables or constants are called Stored Properties
In its simplest form, a stored property is a constant or variable that is stored as part of an instance of a particular class or structure. Stored properties can be either variable stored properties (introduced by the var keyword) or constant stored properties (introduced by the let keyword).
And finally, you have a lazy stored property that maybe can be applied for what you want:
A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.
Solution: create a lazy var stored property or add his properties inside ViewDidLoad (when self already exists)
lazy private var doneButtonItem : UIBarButtonItem = {
[unowned self] in
return UIBarButtonItem(title: "Next", style:UIBarButtonItemStyle.Plain, target: self, action: #selector(onClickNext(button:)))
}()
OR
let rightBarButton: UIBarButtonItem?
override func viewDidLoad() {
super.viewDidLoad()
rightBarButton = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(onClickNext(button:)))
}

import UIKit
class ViewController: UIViewController {
var btnName = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
btnName.setImage(UIImage(named: "imagename"), for: .normal)
btnName.frame = CGRect(x:0,y: 0,width: 30,height: 30)
btnName.addTarget(self, action: #selector(addTargetActionForButton(:_)), for: .touchUpInside)
let rightBarButton = UIBarButtonItem(customView: btnName)
self.navigationItem.rightBarButtonItem = rightBarButton
}
func addTargetActionForButton(_ sender : UIButton){
}
}

Your are taking let variable out side of viewDidLoad You can not take let global variable. if you want to take let than you need to declare inside viewDidLoad. Fore more info let and var check this link What is the difference between `let` and `var` in swift?
override func viewDidLoad() {
let rightBarButton: UIBarButtonItem = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(onClickNext(button:)))
super.viewDidLoad()
self.view.backgroundColor = .white
self.navigationItem.rightBarButtonItem = rightBarButton
}

UIBatButtonItem may contain UIButton
above example image have UINavigationItem which contains LeftBarButtonItems ([UIBarButton]), and RightBarButtonItems([UIBarButtonItem]), each UIBarButtonItem contain UIButton
we can customise the UIButton to specify how to display in View
And we can connect button action directly to the UIButton

Related

Using addTarget(target:action:) without self

Is there any way to use addTarget on something other than self (which seems to be the most common use case)?
Yes, you can use a target other than self. The most common use is to call addTarget with self where self is a reference to the viewController that the adds the UIControl to its view hierarchy. But you aren't required to use it that way. The target is merely a reference to an object, so you can pass it a reference to any object you want. The action is a Selector which needs to be defined as an instance method on the class of that object, and that method must be available to Objective-C (marked with #objc or #IBAction) and it must take either no parameters, just the sender, or the sender and the event.
You can also pass nil as the target, which tells iOS to search up the responder chain for the action method.
Here's a little standalone example:
import UIKit
class Foo {
#objc func buttonClicked() {
print("clicked")
}
}
class ViewController: UIViewController {
let foo = Foo()
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 200, width: 100, height: 30))
button.setTitle("Press me", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(foo, action: #selector(Foo.buttonClicked), for: .touchUpInside)
self.view.addSubview(button)
}
}
You can certainly set up some other object to receive control actions. Consider the following view controller:
First, define a class who's job is to respond to button tap actions:
#objc class ButtonTarget: NSObject {
#IBAction func buttonAction(_ sender: Any) {
print("In \(#function)")
}
}
Now define a view controller that creates a ButtonTarget object
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
lazy var buttonTarget = ButtonTarget() //Give the ViewController a `ButtonTarget`
override func viewDidLoad() {
super.viewDidLoad()
//Add a taret/action to the button that invokes the method `buttonAction(_:)`
button.addTarget(
buttonTarget,
action: #selector(ButtonTarget.buttonAction(_:)),
for: .touchUpInside)
}
}

Why does a UIBarButtonItem action not trigger in a separate class?

I wrote a class that shall handle UIBarButtonItem taps.
The initializer takes a reference to an UINavigationItem. All buttons etc. are attached to this UINavigationItem. I tried to connect them with actions (didPressMenuItem()), but when I click the button, the action is not triggered (nothing is written to the console nor the breakpoint I set is triggered).
How can I link the UIBarButtonItem to the function defined in this class?
internal final class NavigationBarHandler {
// MARK: Properties
private final var navigationItem: UINavigationItem?
// MARK: Initializers
required init(navigationItem: UINavigationItem?) {
self.navigationItem = navigationItem
}
internal final func setupNavigationBar() {
if let navigationItem = navigationItem {
let menuImage = UIImage(named: "menu")?.withRenderingMode(.alwaysTemplate)
let menuItem = UIBarButtonItem(image: menuImage, style: .plain, target: self, action: #selector(didPressMenuItem(sender:)))
menuItem.tintColor = .white
navigationItem.leftBarButtonItem = menuItem
}
}
#objc func didPressMenuItem(sender: UIBarButtonItem) {
print("pressed")
}
}
This is what happens in the view controller to which navigationItem the buttons etc. are attached.
class ContactsController: UIViewController {
// MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
self.title = "Kontakte"
let navigationBarHandler = NavigationBarHandler(navigationItem: self.navigationItem)
navigationBarHandler.setupNavigationBar()
}
}
Th problem here is that you're instantiating NavigationBarHandler inside viewDidload() which is why the memory reference dies after viewDidLoad() finishes. What you should do is to create the variable outside like this.
class ContactsController: UIViewController {
var navigationBarHandler: NavigationBarHandler!
// MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
self.title = "Kontakte"
self.navigationBarHandler = NavigationBarHandler(navigationItem: self.navigationItem)
navigationBarHandler.setupNavigationBar()
}
}
This way the memory reference stays.

Add a button to the native toolbar

I have the following code
final class MyViewController: UIViewController,
UICollectionViewDataSource, UICollectionViewDelegate, /*..*/ {
//
// Declaring local variables, other controllers and delegates etc.
//
override func viewDidLoad() {
super.viewDidLoad()
let item = UIBarButtonItem(title: "A button that should do nothing",
style: .Plain, target: nil, action: nil)
self.navigationController!.toolbar.items = [item]
self.navigationController!.toolbarHidden = false
}
// the rest of my code
}
And no matter what I do, the toolbar appears, yet nothing will render inside of it.
I also tried
self.navigationController?.toolbarItems?.insert(item, atIndex: 0)
to do the insertion of the item, but still no luck.
What am I doing wrong?
Fixed by adding items via
self.toolbarItems = [item]

Why isn't my UIBarButtonItem calling my custom function?

I have a UIViewController with a UIBarButtonItem configured to call my custom function called func settingsTapped(sender: AnyObject?) in which I've put performSegueWithIdentifier. I've determined that this function isn't being called even though the button DOES work and the segue somehow still works because it goes to the correct view controller.
First VC:
class CalculatorViewController: UIViewController {
private let timeInterval = 0.016 //how frequently we change the displayed number
private let animationLength = 0.3
private var timer: NSTimer?
private var counter: Int = 0
private var max: Double = 0.0
var startingPreferredUnits: String?
#IBOutlet weak var weightLifted: UITextField!
#IBOutlet weak var repetitions: UITextField!
#IBOutlet weak var oneRepMax: UILabel!
#IBOutlet weak var percentages: UITextView!
#IBOutlet weak var units: UILabel!
override func viewDidLoad() {
let settingsButton = UIBarButtonItem(title: "Settings", style: .Plain, target: self, action: #selector(settingsTapped(_:)))
self.navigationItem.leftBarButtonItem = settingsButton
super.viewDidLoad()
}
func settingsTapped(sender: AnyObject?) {
startingPreferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
print("In segue, units is \(startingPreferredUnits)") // never prints this caveman debugging
self.performSegueWithIdentifier("segueToSettings", sender: self)
}
}
In the storyboard, I placed a Bar Button Item in the nav bar:
I created a Show (e.g. Push) segue between the Settings bar button item and the Settings view controller and I've given this segue an identifier of 'segueToSettings`. When I touch the Settings button, it does present the Settings view controller, but it doesn't print my caveman debugging line to the console.
I also tried creating the segue between the CalculatorViewController itself and the SettingsViewController (which I think may even be a better way) but when I have it set up that way, nothing happens at all when I touch the Settings button.
I've tried everything I could find on SO but nothing has worked. I hope I don't earn a Stupid Question badge on this one.
UPDATE 1:
I'm still struggling with this one. Here's something else I've learned and tried that didn't work. Clicking the Settings button works by performing the segue from the button to the Settings page which I created in the Storyboard. As one would expect, it will call override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) before doing this. So it's completely ignoring the action: #selector(settingsTapped(_:)) part. Also, I can change the identifier of the segue in the storyboard and it makes no difference at all. It still works. I can even delete the identifier and it works.
I also tried adding another button (this time a barButtonSystemItem like so:
override func viewDidLoad() {
super.viewDidLoad()
let settingsButton = UIBarButtonItem(title: "Settings", style: .Plain, target: self, action: #selector(settingsTapped(_:)))
let saveButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(self.saveThisLift(_:)))
self.navigationItem.leftBarButtonItem = settingsButton
self.navigationItem.rightBarButtonItem = saveButton
}
func saveThisLift(sender: UIBarButtonItem) {
print("I'm in saveThisLift") // never prints
let weight = weightLifted.text
let reps = repetitions.text
let maxAmount = oneRepMax.text
let unitsText = units.description
coreDataStack.saveLiftEvent(currentLiftName!, formula: currentFormulaName!, weight: weight!, repetitions: reps!, maxAmount: maxAmount!, unitsUsed: unitsText)
}
func settingsTapped(sender: AnyObject?) {
startingPreferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
print("In segue, units is \(startingPreferredUnits)") // never prints this caveman debugging
self.performSegueWithIdentifier("segueToSettings", sender: self)
}
Like the Settings button, touching it does nothing except make it flash. I should point out that I added the bar button items via the storyboard and I think that's unnecessary since I'm trying to add them programmatically. However, without them, neither of the buttons appears.
After a lot more research and more trial and error, I've figured it out. I learned a number of things that I'll leave here for anyone who has this problem in the future.
At a high-level, trying to do some of this in the storyboard and some in code made it easy to get confused. The key things for me were:
I wasn't dealing with a UINavigationController with its out-of-the-box root view controller, I was dealing with a UIViewController. With the UINavigationController, you don't have to do as much. But with a UIViewController, I had to add a UINavigationBar and to that, I had to add a single UINavigationItem. I did this by dragging them from the Object Library to my storyboard.
Don't try to put a UILabel in the UINavigationBar to do what I was trying to do, which was have them call custom functions. Trust me, it doesn't work, at least not in my case.
After figuring out I needed to add a UINavigationItem, the next ah-ha! moment for me was the fact that I could put multiple UIBarButtonItems in it (note: my example in my question shows just one to keep it simple but I'm actually adding three items)
The magic piece of the puzzle was connecting my code to the storyboard. I simply Ctrl-clicked from my UINavigationItem to my view controller and created an #IBOutlet called #IBOutlet weak var navItem: UINavigationItem!
My code that creates the buttons and adds them to the view uses this outlet like so (simplified):
_
class CalculatorViewController: UIViewController {
#IBOutlet weak var navItem: UINavigationItem!
override func viewDidLoad() {
super.viewDidLoad()
let settingsButton = UIBarButtonItem(title: "Settings", style: .Plain, target: self, action: #selector(self.segueToSettings(_:)))
let viewLogButton = UIBarButtonItem(title: "Log", style: .Plain, target: self, action: #selector(self.segueToLog(_:)))
let saveButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(self.saveLift(_:)))
self.navItem.leftBarButtonItem = settingsButton
self.navItem.rightBarButtonItems = [saveButton, viewLogButton]
}
func saveLift(sender: AnyObject) {
let weight = weightLifted.text
let reps = repetitions.text
let maxAmount = oneRepMax.text
let unitsText = units.text
coreDataStack.saveLiftEvent(currentLiftName!, formula: currentFormulaName!, weight: weight!, repetitions: reps!, maxAmount: maxAmount!, unitsUsed: unitsText!)
performSegueWithIdentifier("segueToLog", sender: self)
}
}
Lastly, when you create that #IBOutlet, don't name it navigationItem: UINavigationItem because that will make Xcode very unhappy:
I burned a lot of hours on this one. I hope this information helps somebody avoid that in the future.

How do I set title for UIBarButtonItem?

I'm a beginner iPhone developer.
How can I programmatically set the title for the UIBarButtonItem?
My code is the following:
self.navigationItem.rightBarButtonItems =
UIBarButtonItem(
barButtonSystemItem: .Cancel, target: self,
action: "barButtonItemClicked:")
#IBAction func barButtonItemClicked(sender: UIBarButtonItem) {
//print something
}
Use different initialiser that allows you to specify the title:
UIBarButtonItem(title: "title", style: .Plain, target: self, action: "barButtonItemClicked:")
Swift 3.1 Update
UIBarButtonItem(title: "title", style: .plain, target: self, action: #selector(barButtonItemClicked))
Swift 2 answer
You could just add the following to viewDidLoad:
// Some text
self.barButtonItemClicked.title = "Right Button!"
OR
// A Unicode Gear Symbol
// See: http://graphemica.com/%E2%9A%99
self.barButtonItemClicked.title = "\u{2699}"
The ViewController.Swift code below would set the name barButtonItemClicked you used. I used a UIBarButtonItem to show how to set the title. But it is trivial to adapt it for a function.
import UIKit
class ViewController: UIViewController {
// Please make sure the line below is properly connected to your Storyboard!
#IBOutlet weak var barButtonItemClicked: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Set the text of the NavBarButtonItem Title
self.barButtonItemClicked.title = "Right Button!"
// Gear Icon
// self.barButtonItemClicked.title = "\u{2699}"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Swift 4:
#IBOutlet var saveButton: UIBarButtonItem!
saveButton.title = "Saved"
In my case, I wanted to toggle between the Edit|Done.
However, I couldn’t use the leftBarButtonItem because I already had another UIBarButtonItem.
What I did is the following:
1- Create #IBOutlet weak var edit: UIBarButtonItem!
2- Then a variable to hold the state: var isEditingMode = false
3- Now in the viewDidLoad:
override func viewDidLoad() {
…
self.edit.action = #selector(self.toogleEditor(_:))
self.edit.title = "Edit"
self.setEditing(isEditingMode, animated: true)
…
}
I initialize the edit.action Selector to my custom function toogleEditor. I want to be able to change the title whenever an action occur.
4- Create an IBAction:
#IBAction func toogleEditor(sender: AnyObject) {
if isEditingMode
{
isEditingMode = false
self.edit.title = "Edit"
}
else
{
isEditingMode = true
self.edit.title = "Done"
}
self.setEditing(isEditingMode, animated: true)
}
This function is triggered each time the user click the UIBarItemButton.
The only thing to do is use the setEditing(…) to change the behaviour of the UITableViewController.
If anyone wondering how to do it from the storyboard:
Try to edit the "Title" attribute in "Attributes Inspector" as shown below:
from the docs
init(title title: String?,
style style: UIBarButtonItemStyle,
target target: AnyObject?,
action action: Selector)

Resources