iOS Lazy Var Uibarbuttonitem Target Issue

iOS lazy var UIBarButtonItem target issue

Explanation below is my guess. And unfortunately, I don't have enough reputation to give a comment so let me give you an answer.

My guess: this is a compiler bug.


First, I crafted a small extension of UIBarButtonItem. (second parameter is not of Any? but UIViewController?)

extension UIBarButtonItem {
convenience init(barButtonSystemItem systemItem: UIBarButtonSystemItem, targetViewController: UIViewController?, action: Selector?) {
// call the initializer provided by UIKit
self.init(barButtonSystemItem: systemItem, target: targetViewController, action: action)
}
}

Then I tried to initialize the lazy stored variable with the code below.

class ViewController: UIViewController {

lazy var barButtonItem1 = UIBarButtonItem(barButtonSystemItem: .cancel, targetViewController: self, action: #selector(action))

override func viewDidLoad() {
super.viewDidLoad()
print(barButtonItem1.target)
}
func action() { }
}

Then compiler raised error and say

Cannot convert value of type '(NSObject) -> () -> ViewController' to
expected argument type 'UIViewController?'

which suggests that compiler failed to determine that self is of ViewController. (The initializer provided by UIKit would compile because the second parameter is of Any? which accepts value of type (NSObject) -> () -> ViewController.)

But when give type annotation to the lazy variable like

lazy var barButtonItem1: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, targetViewController: self, action: #selector(action))

source code happily compiled and barButtonItem1.target was set to self.

I believe the type annotation helped compile. Above is the reason I guess the issue you faced is caused by a compiler bug.


See also: there are reported problems similar to the issue you faced. Both of them are concluded as a compiler bug.

Swift lazy instantiating using self

Type inference when using lazy instantiation

Swift | UIBarButton not working / not going to action target

The target (self) is still nil when you initialize the volumeButton like you do. Make the button a lazy var instead:

lazy var volumeButton = UIBarButtonItem(image: UIImage(named: "speaker"), style: .plain , target: self, action: #selector(volumeButtonTapped))

As @Robert Dresler stated you also have to replace the second if statement in the volumeButtonTapped function with an else if to make your code work - although this has nothing to do with the target action problem.

You could also make your code a little bit cleaner by using a solution similar to this:

var deviceIsMuted = false {
didSet {
volumeButton.image = UIImage(named: deviceIsMuted ? "mute" : "speaker")
print("volume set to", deviceIsMuted ? "mute" : "loud")
}
}

@objc func volumeButtonTapped(_ sender: Any) {
deviceIsMuted = !deviceIsMuted
}

SWIFT - UIBarButtonItem is not calling action

The problem is that uploadButton is getting initialized too early, i.e. during the initialization of the view controller ifself. At that point, self is not yet ready for use.

There are a few ways to solve this.

  1. Initialize the button in viewDidLoad:
class MyViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
let uploadButton = UIBarButtonItem(title: "UPLOAD", style: .plain, target: self, action: #selector(uploadHandler))
self.navigationItem.setRightBarButton(uploadButton, animated: false)
}
...
}

  1. If some other methods in your class need access to it, slightly modify the code above by creating an implicitly unwrapped stored property and setting it in viewDidLoad:
class MyViewController: UIViewController {

private var uploadButton: UIBarButtonItem!

override func viewDidLoad() {
super.viewDidLoad()
uploadButton = UIBarButtonItem(title: "UPLOAD", style: .plain, target: self, action: #selector(uploadHandler))
self.navigationItem.setRightBarButton(uploadButton, animated: false)
}
...
}

  1. Make the initialization of the button lazy, that way it will be initialized when it's first accessed, i.e. in viewDidLoad:
class MyViewController: UIViewController {

private lazy var uploadButton = UIBarButtonItem(title: "UPLOAD", style: .plain, target: self, action: #selector(uploadHandler))

override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.setRightBarButton(uploadButton, animated: false)
}
...
}

  1. Initialize the button without an action and a selector, add them later in viewDidLoad:
class MyViewController: UIViewController {

private let uploadButton = UIBarButtonItem(title: "UPLOAD", style: .plain, target: nil, action: nil)

override func viewDidLoad() {
super.viewDidLoad()
uploadButton.target = self
uploadButton.action = #selector(uploadHandler)
self.navigationItem.setRightBarButton(uploadButton, animated: false)
}
...
}

There's very little difference among all these options, so it should be fine whatever one you choose.

A couple of side notes:

  • In my examples I declared uploadButton as private to indicate that it is not a part of your view controller's public API. If you need to access this button outside of your view controller, just remove the private modificator.
  • Animation doesn't get performed in viewDidLoad because your view is not yet on screen, so calling setRightBarButton(uploadButton, animated: true) will not trigger the animation, therefore I replaced true with false. If you want the user to see the animation, call it in viewDidAppear.

Why is UIBarButtonItem's target set to nil when initializing with init(barButtonSystemItem, target, action)?

target nil, because you are set the target before controller init.
You should use something like this:

class GameViewController: UIViewController {
var pauseItem: UIBarButtonItem? = nil
var playItem: UIBarButtonItem? = nil

override func viewDidLoad() {
super.viewDidLoad()

playItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(GameViewController.pauseButtonTouchUp))
pauseItem = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(GameViewController.pauseButtonTouchUp))
}
}

UIBarButtonItem selector not working

try with inside scope once

override func viewDidLoad() {
super.viewDidLoad()

let rightButton = UIBarButtonItem(title: "Right", style: .plain, target: self, action: #selector(self.onRightLeftClick(_ :)))
rightButton.tag = 1
let leftButton = UIBarButtonItem(title: "Left", style: .plain, target: self, action: #selector(self.onRightLeftClick(_ :)))
rightButton.tag = 2
self.navigationItem.rightBarButtonItem = rightButton
self.navigationItem.leftBarButtonItem = leftButton
}

handle the action as

func onRightLeftClick(_ sender: UIBarButtonItem){
if sender.tag == 1{
// rightButton action
}else{
// leftButton action
}
}

Swift, confusion with UIBarButtonItem

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:)))
}

UIBarButtonItem not calling function in another class when tapped

scanner should be class level property, otherwise it will be released. Something like this.

class TestViewController: UIViewController {

let scanner = scannerBrain()

override func viewDidLoad() {
super.viewDidLoad()

scanner.parentView = self
let rightButton = UIBarButtonItem(barButtonSystemItem: .add, target: scanner, action: #selector(scanner.startScan))
navigationItem.rightBarButtonItem = rightButton
}
}

Why adding barBarttonItem won’t work if created out of scope where it’s added?

These bar button properties are created before init is called and you're trying to access class methods which can't be called before init is called.

If you want to initialize your buttons on class scope, you will have to make them lazy variables in order to create them when they are needed

lazy var rightButton = UIBarButtonItem(title: "Right", style: .plain, target: self, action: #selector(onRightClick))
lazy var leftButton = UIBarButtonItem(title: "Left", style: .plain, target: self, action: #selector(onLeftClick))


Related Topics



Leave a reply



Submit