Swift | UIButton causes crash on tap - ios

I added a button on the main screen of the app and on the tap of a button, a new viewcontroller is presented.
This works completely fine in the simulator but as soon as I try in an actual iPhone, it causes the app to crash.
Also, the crash is only caused on the login button while the sign up button made the same way does work perfect
I will leave the code below
var loginButton = UIButton()
var signUpButton = UIButton()
loginButton.setTitle("Login", for: .normal)
loginButton.titleLabel?.textAlignment = .center
loginButton.backgroundColor = appGreenTheme
loginButton.titleLabel?.textColor = .white
loginButton.layer.cornerRadius = 20
loginButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
loginButton.setBackgroundImage(UIImage(named: "pinkOrangeGradientPDF"), for: .normal)
loginButton.clipsToBounds = true
signUpButton.setTitle("Sign Up", for: .normal)
signUpButton.setTitleColor(.black, for: .normal)
signUpButton.titleLabel?.textAlignment = .center
signUpButton.backgroundColor = .white
signUpButton.titleLabel?.textColor = .black
signUpButton.layer.cornerRadius = 20
signUpButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
loginButton.addTarget(self, action: #selector(loginButtonTapped1(_:)), for: .allTouchEvents)
signUpButton.addTarget(self, action: #selector(signUpButtonTapped1(_:)), for: .allTouchEvents)
///////////////////////////////////////////////////
#objc func loginButtonTapped1(_ sender: UIButton) {
let nav = UINavigationController(rootViewController: LoginViewController())
self.present(nav, animated: true, completion: nil)
}
#objc func signUpButtonTapped1(_ sender: UIButton) {
let nav = UINavigationController(rootViewController: SignUpViewController())
self.present(nav, animated: true, completion: nil)
}
I also tried with "touchUpInside" events. again it works perfectly in the simulator but not in a physical device.
Any help is welcome.
Below is the error shown in the logs
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SparkGPS.LoginView addTarget:action:forControlEvents:]: unrecognized selector sent to instance 0x13dd4c740'

The answer is in the error message. Somewhere, my guess is in LoginViewController, there is a view of type LoginView. That view is calling addTarget(_:action:for:). LoginView is not subclassed from UIControl and does not have addTarget(_:action:for:). It's causing the crash.
Let me break down the parts of -[SparkGPS.LoginView addTarget:action:forControlEvents:].
The - at the beginning means it's an instance method and not a static or class method.
SparkGPS.LoginView is the module and class. A module is another word for a framework or app. In this case, it looks like you have an app named SparkGPS and a class named LoginView.
addTarget:action:forControlEvents: is Objective-C's name for addTarget(_:action:for:).
Finally, "selector sent to instance" means the variable call a method. Selector is a way to identify a method, and an instance is stored in a variable. For example, in your code you have loginButton.setTitle("Login", for: .normal). This could be worded as setTitle(_:for:) was sent to the instance loginButton.

You can add a tap gesture recogniser to the button itself. It's best practice to use outlets, but this works fine and is useful for other UI components like views or labels too
let loginTapGesture = UITapGestureRecognizer(target: self,
action: #selector(loginButtonTapped1))
loginButton.addGestureRecognizer(loginTapGesture)

Related

Simulator iPhone X Series -keep getting crash unrecognized selector sent to instance with all buttons

I'm on Xcode 10.1 and the simulator is version 10.1 also but it happened with the previous version from last year too. I didn't bother with it because I was building out the basic ui and backend so i skipped it. Now I'm almost ready to launch and now I need to test on the X series.
Whenever I use any of the other regular simulator iPhones from the 5S - 8+ there are no problems, everything is fine, I touch a button and the action happens. I have an iPhone 7+ and the buttons work fine using the actual device.
But whenever I use the simulator and I choose any of the iPhone X series devices when I touch any button in any view controller I always get a crash
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_UIBackButtonContainerView titleLabel]: unrecognized selector sent to instance 0x7fdd99c759e0'
whatever this is causes the problem in the X series [_UIBackButtonContainerView titleLabel]
I'm running Xcode in Debug Mode:
What could be the problem?
Code-
lazy var loginButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Login", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont(name: "ArialRoundedMTBold", size: 19)
// I also tried commenting out button.titleLabel?.font = UIFont(name: "ArialRoundedMTBold", size: 19)
button.backgroundColor = UIColor.lightGray
button.addTarget(self, action: #selector(loginButtonPressed), for: .touchUpInside)
button.clipsToBounds = true
button.layer.cornerRadius = 5
}()
#objc func loginButtonPressed() {
// do something
}
override func viewDidLoad() {
super.viewDidLoad()
// there is a username textfield and a password textField above the button
view.addSubview(loginButton)
loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 8).isActive = true
loginButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16).isActive = true
loginButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16).isActive = true
if UIScreen.main.bounds.width == 320 {
loginButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
} else {
loginButton.heightAnchor.constraint(equalToConstant: 45).isActive = true
}
}
Here is a picture of the button with just a print statement in the action:
Here is a picture of the crash:
I was able to narrow the bug down by a comment #matt made in the comments, he said I'm having quite a lot of trouble understanding what any of the code you've shown could possibly have to do with UIBackButtonContainerView. It seems to me we want to be looking at your back button, not this loginButton. When he said that I looked at other parts of my code.
This LoginVC with the problem is the root vc and I don't have any barButtonItem's in it however I was using this line of code below to hide the text in the backBarButton item in the vc that was getting pushed on so that it would only show a back arrow.
This is in the parent vc that is pushing:
// LoginVC with loginButton inside of it
navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
This is in the child vc that’s getting pushed on:
// child vc that’s getting pushed on
if view.frame.width == 414 && view.frame.height == 896 || view.frame.width == 375 && view.frame.height == 812 {
navigationController?.navigationBar.prefersLargeTitles = true
} else {
navigationController?.navigationBar.prefersLargeTitles = false
}
I have no idea why I can use this with no problem with the iPhone 5,6,7, and 8 series but not the X series. When I commented this out it worked.
To get it to work and to still hide the text in the next vc I had to change the code to:
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
The difference is in the title: parameter.
When it wasn't working I set the parameter to nil.
To get it to work I set it to empty quotes: ""
Very strange problem???
UPDATE upon further inspection the problem has to do with
navigationController?.navigationBar.prefersLargeTitles = true
It seems that you can't use the above code AND set the backBarButton title to nil. I guess Apple wants to make sure some sort of text is there when using large navigationBar titles.
To add a little update to Lance Samaria's Answer. It seems the combination of prefersLargeTitles and navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) causes crashes on certain iOS versions.
From some testing, it looks as though it was fixed on iOS 14, but Apple has also added a new API to achieve the same thing on iOS 14 which doesn't require updating the backBarButtonItem.
In short, to achieve a nil title, whilst still allowing the long-press gesture to bring up the history on iOS 14 and higher you need to add some conditional logic.
We can leverage iOS 14's cleaner API for this.
backButtonDisplayMode = .minimal
Don't use a title, just the back button indicator image.
private func hideBackButtonTitle(for viewController: UIViewController) {
if #available(iOS 14.0, *) {
viewController.navigationItem.backButtonDisplayMode = .minimal
} else {
// Versions < iOS 13 crash when using `prefersLargeTitle` and backButton with a `nil` title.
viewController.navigationItem.backBarButtonItem = UIBarButtonItem(
title: "",
style: .plain,
target: nil,
action: nil
)
}
}

addTarget not working properly

I'm having trouble getting addTarget to work when I swap the buttons programmatically
let leftButton: UIButton = UIButton(type: UIButtonType.custom)
leftButton.setImage(UIImage(named: "menu"), for: UIControlState.normal)
leftButton.frame = CGRect(x: 0, y: 0, width: 24, height: 24)
...
leftButton.addTarget(nil, action: #selector(MapViewController.backToModules(sender:)), for: UIControlEvents.touchUpInside)
...
let leftBarButton = UIBarButtonItem(customView: leftButton)
let rightBarButton = UIBarButtonItem(customView: rightButton)
//assign button to navigationbar
self.navigationItem.rightBarButtonItems = [rightBarButton]
self.navigationItem.leftBarButtonItem = leftBarButton
My target should call the following function, but it doesn't?
func backToModules(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
You are missing target parameter (I assume it's self):
leftButton.addTarget(self, action: #selector(MapViewController.backToModules(sender:)), for: UIControlEvents.touchUpInside)
When adding target, you have to specify selector that determines the identifier of the method to call - you managed to do that. However, you have to also give it a target object, an object on which that selector is supposed to be called - and that you specified as nil. That means that the selector was called on nil object.
In your case, the first target parameter has to be object of type MapViewController on which the method defined by the selector (backToModules(sender:)) will be called. If you call that addTarget method in context of MapViewController instance that should be the target, then use self as I suggested.

Assign 'action' dynamically in uibutton.addTarget

please bear with me, as I'm new to swift -4 weeks old-.
I've created the following 2 functions in fileA.swift
func custombttn(theSelector:Selector)-> UIButton{
let bttn = UIButton(frame: CGRect(x:20, y:400, width:200, height:30))
bttn.setTitle("tap this button", for: UIControlState.normal)
bttn.backgroundColor = UIColor.black
bttn.setTitleColor(UIColor.magenta, for: UIControlState.normal)
bttn.addTarget(bttn, action: theSelector, for: UIControlEvents.touchUpInside)
return bttn
}
func customtxtfld() -> UITextField{
let txtField = UITextField(frame: CGRect(x:20, y:360, width:200, height:30))
txtField.borderStyle = UITextBorderStyle.roundedRect
txtField.backgroundColor = UIColor.magenta
txtField.placeholder = "Do you like me now..?"
return txtField
}
The reason behind the custombttn(theSelector:Selector), is that i want to pass the function dynamically to the button in my viewcontroller file.
Now, moving the fileB.swift, I have the following code...
class TabOneViewController: UIViewController{
let txt = customtxtfld()
let bttn = custombttn(theSelector: #selector(updatetxt))
override func loadView() {
super.loadView()
view.addSubview(txt)
view.addSubview(bttn)
}
func updatetxt(){
txt.text = "hello, you!"
}
}
Here is where things get tricky, when I attempt to build, I don't get any error (not even a warning). However, when I run the app, and tap the bttn in fileB.swift, I get the following error during runtime:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[UIButton updatetxt]:
unrecognized selector sent to instance 0x7f8453415670'
If I have 2 or more functions in my fileB.swift that I wish to assign dynamically to the action part of the addTarget, is there any way I can pass the selector dynamically to a button..?
Appreciate your time and assistance. Please let me know if I need to explain something further.
It's crashing because your button target is wrong.
func custombttn(target:Any, theSelector:Selector)-> UIButton{
let bttn = UIButton(frame: CGRect(x:20, y:400, width:200, height:30))
bttn.setTitle("tap this button", for: UIControlState.normal)
bttn.backgroundColor = UIColor.black
bttn.setTitleColor(UIColor.magenta, for: UIControlState.normal)
bttn.addTarget(target, action: theSelector, for: UIControlEvents.touchUpInside)
return bttn
}
And use it like this
class TabOneViewController: UIViewController{
let txt = customtxtfld()
override func loadView() {
super.loadView()
view.addSubview(txt)
let bttn = custombttn(target:self, theSelector: #selector(updatetxt))
view.addSubview(bttn)
}
func updatetxt(){
txt.text = "hello, you!"
}
}
Yes, you can. The issue here is that you passed the button itself as the target for the action. Just pass the correct target when adding the action, which in this case is the instance of your view controller.

Passing closure in swift as parameter to be used by selector in function

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.

How to add a target action to a UITableViewCell

I've added a button to a specific UITableViewCell. When I select the button, I get a crash:
ButtonTapped
libc++abi.dylib: terminating with uncaught exception of type NSException
At the beginning of cellForRowAt, I'm defining the button:
let myButton = UIButton(type: .custom)
myButton.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
myButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
myButton.tintColor = UIColor.yellow()
For the indexpath.row I attach the button like this:
cell.accessoryView = myButton as UIView
And the action buttonTapped tries to load a different ViewController.
I get confirmation that the button action worked (the routine was called).
The routine is as follows:
func buttonTapped() {
print("ButtonTapped")
let myPickerController = self.storyboard?.instantiateViewController(withIdentifier: "picker") as? MyPickerController
print("1")
self.present(myPickerController!, animated: true)
print("2")
}
As you can see from the log, I do see that the routine was called, but I do not see the print values 1 or 2 before the crash. Anyone see what I'm doing wrong?
Add target like,
myButton.addTarget(self, action: #selector(YourControllerName.buttonTapped(_:)), for: .touchUpInside)
then change your function like,
func buttonTapped(sender : UIButton){
....
}
Hope this helps you.

Resources