Official docs (Swift 4.1) says:
If you use a closure to initialize a property, remember that the rest
of the instance has not yet been initialized at the point that the
closure is executed. This means that you cannot access any other
property values from within your closure, even if those properties
have default values. You also cannot use the implicit self property,
or call any of the instance’s methods.
So I guess i need to use lazy var,today I created a button
let loginRegisterButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = UIColor.rgb(red: 80, green: 101, blue: 161)
button.setTitle("Register", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
button.addTarget(self , action: #selector(handleRegister), for: .touchUpInside)
return button
}()
so I just used self in addTarget method and XCode did not seem to bother even more program went without any errors. Everything worked out well.So Why can i use self when I initialize property with a closure? Is there some changes or maybe I missed something?
The target in your code is probably nil, the reason that this might work is that:
...If you specify nil, UIKit searches the responder chain for an
object that responds to the specified action message and delivers the
message to that object
(from the documentation of the method)
You can verify that the target is actually nil by setting a breakpoint during or after your controller's init and by inspecting the _targetActions array (in the debugger's variables view) of the button (you can see that target's address is 0x0).
In your example, loginRegisterButton is set during the controller's initialization which means that there is no self yet (in other words you cannot guarantee that all instance variables are initialized at this point). The way lazy solves this is that the actual assignment is deferred on first access.
Weirdly—perhaps buggily—the self in your closure evaluates to a function object. (Hamish explained in a comment that the function is a curried form of NSObjectProtocol.self.) However, when you later ask the button for its target, it reports +[NSNull null]:
let loginRegisterButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .red
button.setTitle("Register", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
print(self)
// Output: (Function)
button.addTarget(self, action: #selector(handleRegister), for: .touchUpInside)
let target = button.allTargets.first!.base
print(target, type(of: target))
// Output: <null> NSNull
return button
}()
(Tested using Xcode 9.3.1.)
The function gets turned into NSNull as follows: the function object is allocated just before the call to addTarget, to be passed as the target argument. The loginRegisterButton-creating closure holds a strong reference to the newly-allocated function.
The button stores the target as a zeroing weak reference.
After addTarget returns, the closure releases its strong reference to the function. This was the only strong reference to the function, so Swift deallocates the function. This sets the button's weak reference to the function to nil.
When we then ask the button for allTargets, it constructs an NSSet. Since NSSet cannot hold nil directly, the button puts +[NSNull null] in the set instead.
Related
I am trying to make my button, when tapped, to push to a new View Controller. I've tried many different ways but it won't trigger the function that I have it linked to. I also checked the 3D stack view of my layers and the button is on top and clickable, even when I check the background color, it's not being covered by anything else.
Does anyone have any ideas to what I am doing wrong?
For now I am trying to make the button print out the sentence in the console, however whenever I press it, the string doesn't pop up, so I haven't bothered to connect it to the view controller yet.
Also, I am coding this app without storyboards.
Here is my code below.
It is under the MainPageCell class declared as a UICollectionViewCell
private let playButton: UIButton = {
let button = UIButton()
button.setTitle("", for: .normal)
button.backgroundColor = .clear
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(MainPageCell.buttonTapped), for: .touchUpInside)
return button
}()
#objc func buttonTapped() {
print("I PRESSED THE BUTTON")
}
This line is wrong:
button.addTarget(self, action: #selector(MainPageCell.buttonTapped), for: .touchUpInside)
You cannot assign self as the action target in a property declaration initializer, because the instance designated by self does not exist yet. There is no error or warning (I regard that as a bug), but the action method is never called.
Move that assignment elsewhere and rewrite it, like this:
self.playButton.addTarget(self, action: #selector(MainPageCell.buttonTapped), for: .touchUpInside)
Maybe try defining your button action under the UIView Class, I've had a problem like that before, only worked when i linked it to the View Class, Good luck
I have a view that is placed as a subview of navigationController in order to fill the whole display. On this view I have a subview that has two buttons. "Remove and Done". and then it also has a datepicker. The datePicker works, however, the Remove and Done buttons are not firing the action functions.
The buttons:
var setButton: UIButton = {
var button = UIButton(type: .system)
button.setTitle("Done", for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(handleReminderSetBtn), for: .touchUpInside)
return button
}()
var cancelButton: UIButton = {
var button = UIButton(type: .system)
button.setTitle("Remove", for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(handleReminderCancelBtn), for: .touchUpInside)
return button
}()
The main blackView that is in the navigationController:
viewOverLay.addSubview(cardreminder1)
viewOverLay.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
viewOverLay.backgroundColor = UIColor.black.withAlphaComponent(0.3)
self.navigationController?.view.addSubview(viewOverLay)
CardReminder1 is the UIView on which I have two buttons.
I reckon there is some issue with the target in the addTarget method of the two buttons. What could be the issue?
You shouldn't initialize setButton and cancelButton in that way.
Quoting the Apple documentation from Setting a Default Property Value with a Closure or Function:
If you use a closure to initialize a property, remember that the rest of the instance has not yet been initialized at the point that the closure is executed. This means that you cannot access any other property values from within your closure, even if those properties have default values.
moreover:
You also cannot use the implicit self property, or call any of the instance’s methods, hence the problem is here:
addTarget(self...)
so to fix the issue you should move the buttons initialization (or move the addTarget) after your CardReminder1 is fully initialized.
I just had a similar issue. Along with the other answer posted, making the property lazy worked in my case as well. It depends on when you first try to access that property, but in my case I only accessed it after initialization was complete, so making the property lazy worked perfectly.
For example in your case, the following might work:
lazy var setButton: UIButton = {
var button = UIButton(type: .system)
button.setTitle("Done", for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(handleReminderSetBtn), for: .touchUpInside)
return button
}()
lazy var cancelButton: UIButton = {
var button = UIButton(type: .system)
button.setTitle("Remove", for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(handleReminderCancelBtn), for: .touchUpInside)
return button
}()
Within my app i'm trying to back a UIButton, i just want it to print "this is a test" When i press the button it does the classic button animation, however there is nothing printed in the console.
var tbutton: UIButton = {
let button = UIButton(type: .system)
button.frame = CGRect(x: 40.0, y:400.0, width: 300.0, height: 300.0)
let image = UIImage(named: "backb")
button.setBackgroundImage(image, for: .normal)
button.addTarget(self, action: #selector(dothings), for: .touchUpInside)
return button
}()
#objc func dothings(){
print("this is a test")
}
I then add the button into view with:
view.addSubview(tbutton)
Is there a section of code i'm missing, or have i coded something wrong?
You shouldn't initialize your button in that way.
Quoting the Apple documentation from Setting a Default Property Value with a Closure or Function:
If you use a closure to initialize a property, remember that the rest of the instance has not yet been initialized at the point that the closure is executed. This means that you cannot access any other property values from within your closure, even if those properties have default values. moreover:
You also cannot use the implicit self property, or call any of the instance’s methods, hence the problem is here:
button.addTarget(self, action: #selector(dothings), for: .touchUpInside)
so to fix the issue you should move the button initialization (or move the addTarget) after your ViewController is fully initialized (eg: viewDidLoad).
Another way to fix the issue, assuming you are using such button only after viewDidLoad, is to define it as a lazy var:
A lazy stored property is a property whose initial value is not calculated until the first time it is used
Why in earlier when we invoke self in a computed property like this example we would need to write lazy var but now we don't have to. why?
let(lazy var in earlier times) pauseButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(named: "pause")
button.setImage(image, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .white
button.addTarget(self, action: #selector(handlePause), for: .touchUpInside)
return button
}()
I think there is a misunderstanding, which is what you mentioned in the code snippet is not a computed property! it is just a stored property which has been initialized by a closure; As mentioned in the Swift Initialization -
Setting a Default Property Value with a Closure or Function:
If a stored property’s default value requires some customization or
setup, you can use a closure or global function to provide a
customized default value for that property. Whenever a new instance of
the type that the property belongs to is initialized, the closure or
function is called, and its return value is assigned as the property’s
default value.
You could check: Difference between computed property and property set with closure.
Note that the closure of pauseButton will be executed without even using it, if you tried to check it (add a breakpoint in it), you will notice that. I assume this is not what are your expecting -and not what are you aiming to-, so you should declare it as lazy var instead of let.
However,
Referring to the same Swift documentation:
If you use a closure to initialize a property, remember that the rest
of the instance has not yet been initialized at the point that the
closure is executed. This means that you cannot access any other
property values from within your closure, even if those properties
have default values. You also cannot use the implicit self property,
or call any of the instance’s methods.
Implying that:
class MyViewController: UIViewController {
let btnTitle = "pause"
let pauseButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(named: btnTitle)
button.setImage(image, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .white
button.addTarget(self, action: #selector(handlePause), for: .touchUpInside)
return button
}()
func handlePause() { }
}
Will gives an error on the let image = UIImage(named: btnTitle):
That should also be applicable for any other instance member, for instance, if you would try to add view.addSubview(button) into the closure, you will get the same error for view instance member.
But for a reason (I have no idea why), working with selectors seems to be a special case, because button.addTarget(self, action: #selector(handlePause), for: .touchUpInside) worked fine for me (Xcode 9.0), nevertheless if you tried to add self to it, as:
button.addTarget(self, action: #selector(self.handlePause), for: .touchUpInside)
you would get the following error:
If you use button.addTarget in a regular stored property, you won't get a compile-time error. But I'm pretty sure it is a bug. I experimentally realize that selectors in regular stored properties causes unpredictable results in iOS versions 14.2 and higher. The selector may be released or any other selector that is given in the stored property closure may be associated with the button. As result, tapping on a button may trigger an action that is intended to be triggered by another button.
To do not tussle with such issues, I stick to the old way and use button.addTarget only in lazy stored properties.
I am trying to create a generic button creation function into which I pass a closure that represents the action that results as a result of clicking on the button. My code is below. However, I get the following error:
Argument of #selector cannot refer to property. Any suggestions for a workaround ? I don't want to write separate functions for which everything else is the same except for the target action.
func myButton(textColor tColor:UIColor , title:String,
_ buttonFcn: (UIButton) -> Void,
titleSize:CGFloat=30) -> UIButton {
let newButton = UIButton(type: .System)
let bgColor = UIColor(red:204/255, green:204/255, blue:204/255, alpha:1.0)
newButton.backgroundColor = bgColor
newButton.setTitle(title, forState: .Normal)
newButton.setTitleColor(tColor, forState: .Normal)
newButton.titleLabel?.font = newButton.titleLabel?.font.fontWithSize(titleSize)
newButton.addTarget(self, action:#selector(buttonFcn),
forControlEvents:
UIControlEvents.TouchUpInside)
return newButton
}
The problem is that the target-action mechanism is an Objective-C mechanism, and therefore is predicated on the notion that the action selector is a method of an object. You need, therefore, to have some NSObject-based object that has this function as a method, and which can then serve as the target.
Thus, if what differs in every case is the target and the action, what you need to pass is a reference to the target along with the selector string. Swift will squawk at this, but if you know how to form a selector string correctly you can certainly get away with it; you just won't be able to use the #selector syntax, and so you will risk crashing if you form the selector string incorrectly. But it's the kind of thing we used to do all the time in the old Objective-C days, so go right ahead if that's your aim.
Totally artificial but working example:
func buttonMaker(target:NSObject, selectorString:String) -> UIButton {
let b = UIButton(type:.system)
b.setTitle("Testing", for: .normal)
b.addTarget(target, action: Selector(selectorString), for: .touchUpInside)
b.sizeToFit()
return b
}
And here's how to call it from a view controller:
func doButton(_ sender:Any) {
print("ha!")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let b = buttonMaker(target:self, selectorString:"doButton:")
b.frame.origin = CGPoint(x:100, y:100)
self.view.addSubview(b)
}
And when we tap the button, we don't crash (rather, we print "ha"), because I know how to make selector strings correctly. But, as you can see, to accomplish this I had to give up the use of #selector altogether, so safety is out the window. If I had written my selector string incorrectly — for instance, if I had spelled it wrong, or omitted the colon — we'd have crashed on the button tap, just like we used to all the time before Swift #selector and Objective-C #selector were invented.
If your deployment target is iOS 14 or later, you can use the addAction method instead of addTarget. The addAction method lets you use a closure instead of a selector:
func myButton(
textColor: UIColor,
title: String,
titleSize: CGFloat = 30,
_ handler: #escaping (UIButton) -> Void
) -> UIButton {
let button = UIButton(type: .system)
button.backgroundColor = UIColor(red: 204/255, green: 204/255, blue: 204/255, alpha: 1.0)
button.setTitle(title, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.titleLabel?.font = button.titleLabel?.font.withSize(titleSize)
let action = UIAction { action in
guard let button = action.sender as? UIButton else { return }
handler(button)
}
button.addAction(action, for: .touchUpInside)
return button
}
iOS 14 was released on 2020-09-16 and supports iPhone 6S and later devices.