Swift: Accept a closure with varying parameters - ios

Suppose I have a function that accepts a callback with a sender, like this:
func performAction(aNumber: Double, completion: (sender: UIButton) -> Void) {
// Does some stuff here
let button = getAButtonFromSomewhere()
completion(button)
}
And so one possible way to call this function is by passing an existing function for the callback, rather than defining the closure in-place:
performAction(10, completion: myCallback)
func myCallback(sender: UIButton) {
sender.setTitle("foo", forState: .Normal)
}
Back in my definition for performAction, how can I define the completion block to accept a UIButton or any subclass of it?
As an example, suppose I have a UIButton subclass called CustomButton. So in my callback, I'm only interested in accepting a CustomButton. I'd like to do this:
performAction(10, completion: myCallback)
// This produces a compiler error:
func myCallback(sender: CustomButton) {
sender.setTitle("foo", forState: .Normal)
}
// This works, but forces me to cast to my custom class:
func myCallback(sender: UIButton) {
let realButton = sender as! CustomButton
realButton.setTitle("foo", forState: .Normal)
}
But the compiler won't allow it, because the definition of performAction requires the callback to accept a UIButton specifically (even though CustomButton is a UIButton subclass).
I'd like performAction to be generic so that it can be packaged in a library, and work with any UIButton subclass. Is this possible to do in Swift?
EDIT: I tried to simplify what I'm doing with the example above, but I think it just caused confusion. Here's the actual code that I'm trying to make work, with some improvements thanks to #luk2302:
public extension UIButton {
private class Action: AnyObject {
private var function: Any
init(function: Any) {
self.function = function
}
}
// Trickery to add a stored property to UIButton...
private static var actionsAssocKey: UInt8 = 0
private var action: Action? {
get {
return objc_getAssociatedObject(self, &UIButton.actionsAssocKey) as? Action
}
set(newValue) {
objc_setAssociatedObject(self, &UIButton.actionsAssocKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
internal func performAction(sender: UIButton) {
if let function = self.action!.function as? () -> Void {
function()
// THIS IS WHERE THINGS BREAK NOW:
} else if let function = self.action!.function as? (sender: self.Type) -> Void {
function(sender: self)
}
}
public func addTarget(forControlEvents event: UIControlEvents, action: () -> Void) {
self.action = Action(function: action)
self.addTarget(self, action: "performAction:", forControlEvents: event)
}
public func addTarget<B: UIButton>(forControlEvents: UIControlEvents, actionWithSender: (sender: B) -> Void) {
self.action = Action(function: actionWithSender)
self.addTarget(self, action: "performAction:", forControlEvents: forControlEvents)
}
}
The only piece that breaks now is the line that I commented, at (sender: self.Type) (self being either UIButton, or some subclass of it).
So this deviates from my original question slightly, but how can I can I cast function to a closure accepting a sender of the same type as self? This code works perfectly if I hard-code the type, but it should be able to work for any UIButton subclass.

You can make the UIButton subclass a generic parameter for the performAction function, but then you will need to cast the button before passing it to the callback, unless you also have a generic way of "getting" the right type of button.
// performAction() works with any type of UIButton
func performAction<B: UIButton>(aNumber: Double, completion: (sender: B) -> Void)
{
// Assuming getAButtonFromSomewhere returns UIButton, and not B, you must cast it.
if let button = getAButtonFromSomewhere() as? B {
completion(sender: button)
}
}

Ask yourself this: what should happen if you pass a closure of type (CustomButton -> Void) as completion and then getAButtonFromSomewhere returns an instance of UIButton? The code then cannot invoke the closure since the UIButton is not a CustomButton.
The compiler simply does not allow you to pass (CustomButton -> Void) to (UIButton -> Void) because (CustomButton -> Void) is more restrictive than (UIButton -> Void). Note that you can pass (UIButton -> Void) to a closure of type (CustomButton -> Void) since (UIButton -> Void) is less restrictive - you can pass everything you pass to the first to the second as well.
Therefore either make func use a generic type as #jtbandes suggests or use your initial approach a little bit improved:
func myCallback(sender: UIButton) {
if let realButton = sender as? CustomButton {
realButton.setTitle("foo", forState: .Normal)
}
}
Both solutions will result in the setTitle to not be invoked whenever the returned value of getAButtonFromSomewhere is not a CustomButton.

Related

Selectors in Swift 4.0

I was wondering if there is a more 'swift 4' way of creating a selector and calling a function? I am wanting to have the click of the Status Bar Button to call a simple print command from a function, but is this outdated or is there a more efficient 'swift' way of doing this?
button.action = #selector(myFunction)
#objc func myFunction (sender: NSStatusBarButton) {
print("Hi")
}
I am not sure if there's any good way to avoid using the target/action pattern under the hood, but you can definitely try to hide it.
Personally I use ReactiveSwift for all callbacks so I never have to use this awkward objc syntax. Another way to do it would be to hide this inside an extension. For instance, you can try something like:
extension UIButton {
private struct AssociatedKeys {
static var TouchUpClosure = "touchUpClosure"
}
internal var onTouchUpInside: ((UIButton) -> ())? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.TouchUpClosure) as? (UIButton) -> ()
}
set {
objc_setAssociatedObject(
self,
&AssociatedKeys.TouchUpClosure,
newValue as? (UIButton) -> (), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
button.action = #selector(executeTouchUpInside:)
}
}
#objc func executeTouchUpInside(sender: UIButton) {
self.touchUpInside(sender)
}
}
Which allows you to use a "more swift" syntax (no #objc or #selector):
button.onTouchUpInside = { _ in print("Hi") }
Disclaimer - I haven't checked if this exact code compiles, this post is more about sharing an idea.

Passing arguments to selector in Swift

I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}

ReactiveSwift - Bind `Action` to `UIControl`

I am trying to bind an Action on a UISwitch.
I have created the Action using the following code
action = Action<UISwitch, Bool, NoError> { (input: UISwitch) -> SignalProducer<Bool, NoError> in
return SignalProducer{ (observer, disposable) in
observer.send(value: input.isOn)
observer.sendCompleted()
}
}
but I am having trouble connecting it to the UISwitch.
Can someone help?
there is another class CocoaAction that is used to wrap Actions and connect them to UIControls (now in ReactiveCocoa and not in core ReactiveSwift, so if you are using RAC 5 you will have to import both)
var switch: UISwitch!
//switch.addTarget() does not retain the target, so if we do not
//keep a strong reference here the cocoaAction will be deallocated
//at the end of viewDidLoad() and you will get unrecognized selector
//errors when the switch tries to execute the action
var switchCocoaAction: CocoaAction!
override func viewDidLoad() {
let action = Action<UISwitch, Bool, NoError> { (input: UISwitch) -> SignalProducer<Bool, NoError> in
return SignalProducer { (observer, disposable) in
observer.send(value: input.isOn)
observer.sendCompleted()
}
}
//unsafe because it will cast anyObject as! UISwitch
self.switchCocoaAction = action.unsafeCocoaAction
switch.addTarget(switchCocoaAction,
action: CocoaAction.selector,
forControlEvents: .ValueChanged
)
}
however, if all you want is a signal emitting the switch.isOn value whenever it changes, you can accomplish that much more easily using the builtin rac_signalForControlEvents
func switchSignal() -> SignalProducer<Bool, NoError> {
switch.rac_signalForControlEvents(.ValueChanged) //returns legacy RACsignal
.toSignalProducer() //legacy RACSignal -> swift SignalProducer
.flatMapError { _ in .empty } //NSError -> NoError, errors not possible here, so ignore them
.map { ($0 as! UISwitch).isOn }
}

pass a UIButton into a function

I have a function that runs when a button is pressed.
Inside that function is another function.
I would like to pass the pressed button into the inner function so that I can change the text of the button depending on stuff in that inner function.
#IBAction func newItem(sender: AnyObject) {
let urlFetch:String = urlField.text!
self.service.createNewItem(urlFetch, I_WANT_BUTTON_HERE)
}
How do I pass the button into the function?
If it helps this is the function that I am passing it to:
func createNewItem(item_url: String, plus_button: UIButton) {
let dataDictionary = ["url" : item_url]
self.post("item/create", data: dataDictionary).responseJSON { (response) -> Void in
plus_button.titleLabel!.text = "success"
}
}
Later I will add an if statement that changes the text depending on what the response is.
The object passed into newItem method is actually the button you tapped so you can securely convert the type of parameter sender into UIButton like below:
#IBAction func newItem(sender: UIButton) {
...
self.service.createNewItem(urlFetch, sender)
}
There is also one thing though. Setting the text of titleLabel is not the right way of updating the title of button. You should be using setTitle:forState instead:
func createNewItem(item_url: String, plus_button: UIButton) {
...
plus_button.setTitle("success", forState: .Normal)
}
Instead of sending AnyObject as parameter in the newItem function, pass a UIButton.
#IBAction func newItem(sender: UIButton) {
let urlFetch:String = urlField.text!
self.service.createNewItem(urlFetch, sender)
}

Strongly typed selectors in swift

I wonder can we implement strongly typed selectors in swift? For example if i have a method named buttonTapped(sender: AnyObject) in my view controller later when we add that method as target to some button can we just say
button.addTarget(self, selector:ViewController.buttonTapped(self), forControlEvents: .TouchUpInside)
Outdated. See Claus Jørgensen's answer for Swift 2.2+
There is no concept of "selector" in Swift. Ideally, closure should be used for such purpose.
What you really want is something like this
button.addAction(forControlEvents: .TouchUpInside) {
// watch out for retain cycle, use weak or unowned accordingly
ViewController.buttonTapped(self)
}
and you can have it with this code (untested, but it should give you a start point)
public class ClosureWrapper : NSObject
{
let _callback : Void -> Void
init(callback : Void -> Void) {
_callback = callback
}
public func invoke()
{
_callback()
}
}
var AssociatedObjectHandle: UInt8 = 0
extension UIControl
{
public func addAction(forControlEvents events: UIControlEvents, withCallback callback: Void -> Void)
{
let wrapper = ClosureWrapper(callback)
addTarget(wrapper, action:"invoke", forControlEvents: events)
// as #newacct said in comment, we need to retain wrapper object
// this only support 1 target, you can use array to support multiple target objects
objc_setAssociatedObject(self, &AssociatedObjectHandle, wrapper, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
}
}
and hopefully in the future release of SDK, similar methods takes closure instead of selectors will be added by Apple, so we don't need to implement them ourself.
Swift 2.2 have compile time checked selectors using the #selector syntax, see https://swift.org/blog/swift-2-2-new-features/ and Using Swift with Cocoa and Objective-C (Swift 2.2)
For your example, it would be written this way:
button.addTarget(self, selector: #selector(ViewController.buttonTapped(_:)), forControlEvents: .TouchUpInside)
This is my Swift 2.0 version of the Bryan's great answer:
import UIKit
public class CallbackHolder: NSObject {
let callback: () -> Void
init(callback: () -> Void) {
self.callback = callback
}
public func call() {
callback()
}
}
private let AssociatedObjectHandle = UnsafePointer<Void>()
extension UIButton {
func addAction(forControlEvents events: UIControlEvents, withCallback callback: () -> Void) {
let holder = CallbackHolder(callback: callback)
addTarget(holder, action: "call", forControlEvents: events)
objc_setAssociatedObject(self, AssociatedObjectHandle, holder, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // prevent garbage collection
}
}

Resources