Swift UIControl target actions, how to remove for specific classes? - ios

I am using such code to add target-actions to UIControl (like UIButton) as closure.
textField.addTarget(for: .editingChanged) { [weak self] sender in
}
func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: #escaping (_ sender: Any) ->()) {
let key = String(describing: controlEvent)
let target = Target(target: target)
addTarget(target, action: target.action, for: controlEvent)
property[key] = target
}
Adding event handler as closure is important as I am using this target-action inside protocol extension, so cannot define regular target action method here with #objc modifier inside this extension.
I am adding this event handler while scrolling UITableView and need to remove this target-actions when cells are disappearing from screen.
I think I can remove just all target actions
But I rather want to be more precise and just remove Target class target-actions.
It seems that this method should work and doesn't interfere with other target-actions added in another places in code but I have some doubts
func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
let key = String(describing: controlEvent)
let target = property[key]
print("\(key)")
print("\(target)")
removeTarget(target, action: target?.action, for: controlEvent)
I have also tried to use something like this but this doesn't work as first param should be rater target object not class type
textField.removeTarget(Target.self, action: nil, for: .editingChanged)
textField.removeTarget(Target.self, action: nil, for: .editingDidEnd)
Any ideas? It seems that I am constantly adding new target-actions and then have several callbacks invoked also for TableView's cells that already disappeared.

Related

Pass parameters to button action in swift

When setting the action with the "addTarget" method on a button in Swift, is there a way for me to pass a parameter to the function I want to trigger?
Say I had a simple scenario like this:
let button = UIButton()
button.addTarget(self, action: #selector(didPressButton), for: .touchUpInside)
#objc func didPressButton() {
// do something
}
Obviously the above code works fine, but say I wanted the 'didPressButton' function to take in a parameter:
#objc func didPressButton(myParam: String) {
// do something with myParam
}
Is there a way I can pass a parameter into the function in the 'addTarget' method?
Something like this:
let button = UIButton()
button.addTarget(self, action: #selector(didPressButton(myParam: "Test")), for: .touchUpInside)
#objc func didPressButton(myParam: String) {
// do something with myParam
}
I'm coming from a JavaScript background and in JavaScript it's pretty simple to achieve this behavior by simply passing an anonymous function that would then call the 'didPressButton' function. However, I can't quite figure how to achieve this with swift. Can a similar technique be used by using a closure? Or does the '#selector' keyword prevent me from doing something like that?
Thank you to anyone who can help!
The short answer is no.
The target selector mechanism only sends the target in the parameters. If the string was a part of a subclass of UIbutton then you could grab it from there.
class SomeButton: UIButton {
var someString: String
}
#objc func didPressButton(_ button: SomeButton) {
// use button.someString
}
It is not possible to do that in iOS. You can get the View in the selector in your case it is a button.
button.addTarget(self, action: #selector(buttonClick(_:)), for: .touchUpInside)
#objc func buttonClick(_ view: UIButton) {
switch view.titleLabel?.text {
case "Button":
break
default:
break
}
}

Why is the new iOS 14 UIControl action syntax so terrible?

New in iOS 14, we can attach an action handler directly to a UIControl:
let action = UIAction(title:"") { action in
print("howdy!")
}
button.addAction(action, for: .touchUpInside)
That's cool in its way, but the syntax is infuriating. I have to form the UIAction first. I have to give the UIAction a title, even though that title will never appear in the interface. Isn't there a better way?
First, you don't need to supply the title. This is (now) legal:
let action = UIAction { action in
print("howdy!")
}
button.addAction(action, for: .touchUpInside)
Second, you don't really need the separate line to define the action, so you can say this:
button.addAction(.init { action in
print("howdy!")
}, for: .touchUpInside)
However, that's still infuriating, because now I've got a closure in the middle of the addAction call. It ought to be a trailing closure! The obvious solution is an extension:
extension UIControl {
func addAction(for event: UIControl.Event, handler: #escaping UIActionHandler) {
self.addAction(UIAction(handler:handler), for:event)
}
}
Problem solved! Now I can talk the way I should have been permitted to all along:
button.addAction(for: .touchUpInside) { action in
print("howdy!")
}
[Extra info: Where's the sender in this story? It's inside the action. UIAction has a sender property. So in that code, action.sender is the UIButton.]

UIButton add target without sender

I have a UIDatePicker. After a date is selected I call this method
func goToNextScreen(selectedDate: Date) {
//...
}
Now I have added a UIButton. In that button action I want to call the same method goToNextScreen without any date value. Date value is optional in next screen. I tried the following code
btn.addTarget(self, action: #selector(goToNextScreen), for: .touchUpInside)//goToNextScreen(_:)
#objc func goToNextScreen(selectedDate: Date? = nil) {
//...
}
When the button is tapped the app crashes.
How to solve this without adding another method? If it is not possible why my approach doesn't work
What is happening here is, the button's internal logic is trying to pass the sender, which is a UIButton into your method's Date parameter. However, the sender parameter won't get passed if your method don't have any arguments.
Optional parameters don't really work in this situation. What you can do however, is to create another parameterless overload for goToNextScreen:
#objc func goToNextScreen() {
goToNextScreen(selectedDate: nil)
}
And change
btn.addTarget(self, action: #selector(goToNextScreen), for: .touchUpInside)
to
btn.addTarget(self, action: #selector(goToNextScreen as () -> Void), for: .touchUpInside)
so that it different between the two overloads.
Note that the reason why just writing #selector(goToNextScreen) is ambiguous is because you have two methods named goToNextScreen, and Swift needs to resolve to one of them. But it can't with just the name. Here is a similar situation:
class Foo {
#objc func f() {}
func f(x: Int) {}
let selector: Selector = #selector(f) // ambiguous use of f
}
Edit: You can't really do this without creating another method. Selectors are inflexible things.
It's not possible in this case re-use the same method. You should create a new one without parameters or whose parameters are UIButton (or a more generic type, often is Any) and the UIEvent.
Here's the explanation of the Target-Action mechanism: UIControl.
I thought you've to try this
var selectedDate = Date()
btn.addTarget(self, action: #selector(goToNextScreen), for: .touchUpInside)//goToNextScreen(_:)
#objc func goToNextScreen(_ sender: UIButton)
{
selectedDate ?? self.datePickerDate : Date()
}
Using Any as sender type and casting to Date works
//add button Target
btn.addTarget(self, action: #selector(goToNextScreen(_:)), for: .touchUpInside)
//Call with date value
goToNextScreen(Date())
#objc func goToNextScreen(_ selectedDate: Any) {
nextVC.date = selectedDate as? Date
//...
}
sender paramter of UIButton is casted to Date which is reason of crash , it should be a UIButton
btn.addTarget(self, action: #selector(btnClicked), for: .touchUpInside)//goToNextScreen(_:)
#objc func btnClicked(_ sender:UIButton) {
// call next here
goToNextScreen()
}
func goToNextScreen(_ selectedDate: Date? = nil) {
if let date = selectedDate { }
}

store #selector in a variable

I'm adding a target to a button but instead of having the target action reference a predefined function I want it to reference a closure.
typealias AlertAction = (title: String, handler: () -> ())
class ErrorView: UIView {
func addAction(_ action: AlertAction) {
let button = UIButton()
...
let selector = #selector(action.handler) //error happens here
button.addTarget(self, action: selector, for: .touchUpInside)
}
}
I'm getting an error on this line:
let selector = #selector(action.handler)
which is "Argument of '#selector' does not refer to an '#objc' method, property, or initializer"
This makes sense because usually you have to add #objc to your func declaration, but I'm wondering if there's a way to make my closure refer to an #objc method after the fact perhaps by wrapping it in another function.
Is this possible? I don't know how to define an #objc marked closure so I'm not sure.
#selector() is based on Objective-C bridging, since swift closures are non-objective-c, you can't use them.
One alternative solution is wrap your code inside an Objective-C function.
class AlertAction:NSObject {
var title:String?
#objc
func getHandler(sender:Any){
print("hi")
}
}
and use it like:
// Instance of a class
let alertAction = AlertAction()
// Usage
let button = UIButton()
let selector1 = #selector(AlertAction.getHandler)
button.addTarget(alertAction, action: selector1, for: .touchUpInside)

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.

Resources