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.
- 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)
}
...
}
- 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)
}
...
}
- 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)
}
...
}
- 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 theprivate
modificator. - Animation doesn't get performed in
viewDidLoad
because your view is not yet on screen, so callingsetRightBarButton(uploadButton, animated: true)
will not trigger the animation, therefore I replacedtrue
withfalse
. If you want the user to see the animation, call it inviewDidAppear
.
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
Added a Custom Framework, Now Swift Can't Unarchive Data
Swift - Weekday by Current Location by Currentcalendar()
iOS - Emphasise with Bold Strings in Localizable.Strings
Fetch() Doing Get Instead of Post on React-Native (iOS)
Swift Playground and Simulator Error (Ipc/Mig) Server Died, Unable to Boot The iOS Simulator
Avplayer Removing Observer Crash in Swift 2.2
Does "Let _ = ..." (Let Underscore Equal) Have Any Use in Swift
Simulator Vs Physical Device: Navigationlink Broken After One Use
How to Upload Multiple Images in Multipart Using Alamofire
iPhone - Memory Leak - Nsdata Datawithcontentsofurl & UIwebview
Fastlane Boarding with Two-Factor Authentication
Atos Does Not Symbolicate System Frameworks/Libraries Properly
Retrieve All Child Value to a Label in Table View Cell
Stop Overscroll When Using "-Webkit-Overflow-Scrolling: Touch"
How to Keep Spritekit Scene Paused When App Becomes Active