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
}
}
Related
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.
I want to make a selector argument of my method refer to a closure property, both of them exist in the same scope. For example,
func backgroundChange() {
self.view.backgroundColor = UIColor.blackColor()
self.view.alpha = 0.55
let backToOriginalBackground = {
self.view.backgroundColor = UIColor.whiteColor()
self.view.alpha = 1.0
}
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(backToOriginalBackground), userInfo: nil, repeats: false)
}
However, this shows an error: Argument of #selector cannot refer to a property.
Of course I can define a new, separate method and move the implementation of the closure to it, but I want to keep it frugal for such a small implementation.
Is it possible to set a closure to #selector argument?
Not directly, but some workarounds are possible. Take a look at the following example.
/// Target-Action helper.
final class Action: NSObject {
private let _action: () -> ()
init(action: #escaping () -> ()) {
_action = action
super.init()
}
#objc func action() {
_action()
}
}
let action1 = Action { print("action1 triggered") }
let button = UIButton()
button.addTarget(action1, action: #selector(action1.action), forControlEvents: .TouchUpInside)
I tried this for UIBarButtonItem at least:
private var actionKey: Void?
extension UIBarButtonItem {
private var _action: () -> () {
get {
return objc_getAssociatedObject(self, &actionKey) as! () -> ()
}
set {
objc_setAssociatedObject(self, &actionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
convenience init(title: String?, style: UIBarButtonItemStyle, action: #escaping () -> ()) {
self.init(title: title, style: style, target: nil, action: #selector(pressed))
self.target = self
self._action = action
}
#objc private func pressed(sender: UIBarButtonItem) {
_action()
}
}
Then you can do this:
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, action: {
print("Hello World!")
})
As #gnasher729 notes, this is not possible because selectors are just names of methods, not methods themselves. In the general case, I'd use dispatch_after here, but in this particular case, the better tool IMO is UIView.animateWithDuration, because it's exactly what that function is for, and it's very easy to tweak the transition:
UIView.animateWithDuration(0, delay: 0.5, options: [], animations: {
self.view.backgroundColor = UIColor.whiteColor()
self.view.alpha = 1.0
}, completion: nil)
It is now possible. I've created a gist for block-based selectors in Swift 4.
https://gist.github.com/cprovatas/98ff940140c8744c4d1f3bcce7ba4543
Usage:
UIButton().addTarget(Selector, action: Selector { debugPrint("my code here") }, for: .touchUpInside)`
You can use ActionClosurable which support UIControl, UIButton, UIRefreshControl, UIGestureRecognizer and UIBarButtonItem.
https://github.com/takasek/ActionClosurable
Bellow show example of UIBarButtonItem
// UIBarButtonItem
let barButtonItem = UIBarButtonItem(title: "title", style: .plain) { _ in
print("barButtonItem title")
}
#werediver's answer is excellent. Here's an update that allows you to call it as a function.
import Foundation
public extension Selector {
/// Wraps a closure in a `Selector`.
/// - Note: Callable as a function.
final class Perform: NSObject {
public init(_ perform: #escaping () -> Void) {
self.perform = perform
super.init()
}
private let perform: () -> Void
}
}
//MARK: public
public extension Selector.Perform {
#objc func callAsFunction() { perform() }
var selector: Selector { #selector(callAsFunction) }
}
You need to manage strong references to Selector.Performs. One way to do that is to subclass UIKit classes that were designed to work with target-action:
/// A `UITapGestureRecognizer` that wraps a closure.
public final class TapGestureRecognizer: UITapGestureRecognizer {
public init(_ perform: #escaping () -> Void) {
self.perform = .init(perform)
super.init(target: self.perform, action: self.perform.selector)
}
public let perform: Selector.Perform
}
let tapRecognizer = TapGestureRecognizer { print("🍔🐈") }
tapRecognizer.perform() // "🍔🐈"
No, #selector refers to an Objective-C method.
You can do something much better though: Add an extension to NSTimer that lets you create a scheduled timer not with a target and selector, but with a closure.
If you change the scope of block to a class scope rather than function and hold a reference to closure there.
You could invoke that closure with a function. in the class. So that way you can invoke that closure as a selector.
Something like this:
class Test: NSObject {
let backToOriginalBackground = {
}
func backgroundChange() {
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(test), userInfo: nil, repeats: false)
}
func test() {
self.backToOriginalBackground()
}
}
My solution was to create a class block variable like:
let completionBlock: () -> () = nil
Create a method which calls this completionBlock:
func completed(){
self.completionBlock!()
}
And inside where I want to put my selector like a block I did:
func myFunc(){
self.completionBlock = {//what I want to be done}
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(Myclass.completed), userInfo: nil, repeats: false)
}
So my answer to having a selector be assigned to a closure in a swift like manner is similar to some of the answers already, but I thought I would share a real life example of how I did it within a UIViewController extension.
fileprivate class BarButtonItem: UIBarButtonItem {
var actionCallback: ( () -> Void )?
func buttonAction() {
actionCallback?()
}
}
fileprivate extension Selector {
static let onBarButtonAction = #selector(BarButtonItem.buttonAction)
}
extension UIViewController {
func createBarButtonItem(title: String, action: #escaping () -> Void ) -> UIBarButtonItem {
let button = BarButtonItem(title: title, style: .plain, target nil, action: nil)
button.actionCallback = action
button.action = .onBarButtonAction
return button
}
}
// Example where button is inside a method of a UIViewController
// and added to the navigationItem of the UINavigationController
let button = createBarButtonItem(title: "Done"){
print("Do something when done")
}
navigationItem.setLeftbarButtonItems([button], animated: false)
Swift 5.2.x
First of all, you need to declare an "easy to use" typealias for your block:
typealias Completion = () -> ()
Then, you must declare private var to use "as a gate" for your function:
private var action: Completion?
After that, you should create a function that can be called by your Selector (it accept only string format) and to call private completion:
#objc func didAction() {
self.action?()
}
Finally you can re-write your function (using the new swift syntax) like:
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(didAction), userInfo: nil, repeats: false)
self.action = backToOriginalBackground
P.S.: Remember that your variable (or parameter if you embed it to a function) must be of the same of type declared to your typeAlias so, in our case:
var backToOriginalBackground: () -> ()
or also:
var backToOriginalBackground: Completion
It has been several years since this question was asked, and it is worth noting that in those years, Apple has added variants of many selector-using methods that take closures instead.
The original question asks about NSTimer.scheduledTimerWithTimeInterval. That method is now spelled Timer.scheduledTimer and has a version that takes a closure. So the function in the original question can be rewritten thus:
extension UIViewController {
func changeBackground() {
self.view.backgroundColor = .black
self.view.alpha = 0.55
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
self.view.backgroundColor = .white
self.view.alpha = 1.0
}
}
}
Here are some other common cases where, as of May 2016, a selector was required, but which can now use a closure:
UIControl now has an addAction method that takes a UIAction, and UIAction takes a closure. Subclasses of UIControl include UIButton, UISwitch, and UITextField.
UIBarButtonItem has an initializer that takes a UIAction.
NotificationCenter now has an addObserver method that takes a closure. It also supports Combine (the publisher method) and async/await (the notifications method).
RunLoop now has a perform method that takes a closure.
I have what I thought to be a very simple protocol extension for my UIViewControllers providing the capability to dismiss a keyboard through a tap gesture. Here's my code:
#objc protocol KeyboardDismissing {
func on(tap: UITapGestureRecognizer)
}
extension KeyboardDismissing where Self: UIViewController {
func addDismissalGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(Self.on(tap:)))
view.addGestureRecognizer(tap)
}
func on(tap: UITapGestureRecognizer) {
dismissKeyboard()
}
func dismissKeyboard() {
view.endEditing(true)
}
}
The problem is that the above code throws a compile error on this line:
let tap = UITapGestureRecognizer(target: self, action: #selector(Self.on(tap:)))
With the error message:
Argument of '#selector' refers to instance method 'on(tap:)' that is not exposed to Objective-C
with the suggestion to "fix it" by adding #objc before func on(tap: UITapGestureRecognizer)
Ok fine, I add the tag:
#objc func on(tap: UITapGestureRecognizer) {
dismissKeyboard()
}
But then, it throws a different compile error on this newly added #objc tag with the error message:
#objc can only be used with members of classes, #objc protocols, and concrete extensions of classes
with the suggestion to "fix it" by removing the exact same tag I was just told to add.
I originally thought adding #objc before my protocol definition would solve any #selector problems but apparently that's not the case, and these cyclical error messages/suggestions aren't helping in the slightest. I've gone down a wild goose chase of adding/removing #objc tags everywhere, marking methods as optional, putting methods in the protocol's definition, etc.
It also doesn't matter what I put in the protocol definition Leaving the extension the same, the following example does not work nor does any combination of the declared methods in the protocol's definition:
#objc protocol KeyboardDismissing {
func on(tap: UITapGestureRecognizer)
}
This tricks me into thinking it works by compiling as a stand alone protocol, but the second I try to add it to a view controller:
class ViewController: UIViewController, KeyboardDismissing {}
it spits back the original error.
Can someone explain what I'm doing wrong and how I can compile this?
Note:
I've looked at this question but it is for Swift 2.2 not Swift 3 nor does the answer compile as soon as you create a view controller class that inherits from the protocol defined in the example.
I've also looked at this question but the answer uses NotificationCenter which is not what I'm after.
If there are any other seemingly duplicate questions, please let me know.
Matt's answer is correct. However, I would just add that, if you are dealing with #selector to use from a NotificationCenter notification, you could try to avoid #selector by using the closure version.
Example:
Instead of writing:
extension KeyboardHandler where Self: UIViewController {
func startObservingKeyboardChanges() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
// !!!!!
// compile error: cannot be included in a Swift protocol
name: .UIKeyboardWillShow,
object: nil
)
}
func keyboardWillShow(_ notification: Notification) {
// do stuff
}
}
you could write:
extension KeyboardHandler where Self: UIViewController {
func startObservingKeyboardChanges() {
// NotificationCenter observers
NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: nil) { [weak self] notification in
self?.keyboardWillShow(notification)
}
}
func keyboardWillShow(_ notification: Notification) {
// do stuff
}
}
This is a Swift protocol extension. Swift protocol extensions are invisible to Objective-C, no matter what; it knows nothing of them. But #selector is about Objective-C seeing and calling your function. That is not going to happen because your on(tap:) function is defined only in the protocol extension. Thus the compiler rightly stops you.
This question is one of a large class of questions where people think they are going to be clever with protocol extensions in dealing with Cocoa by trying to inject Objective-C-callable functionality (selector, delegate method, whatever) into a class via a protocol extension. It's an appealing notion but it's just not going to work.
As Matt said, you can't implement #objc methods in a protocol. Frédéric's answer covers Notifications, but what can you do about standard Selectors?
Let's say you have a protocol & extension, like so
protocol KeyboardHandler {
func setupToolbar()
}
extension KeyboardHandler {
func setupToolbar() {
let toolbar = UIToolbar()
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: self,
action: #selector(self.donePressed))
}
#objc func donePressed() {
self.endEditing(true)
}
}
This will generate an error, as we know. What we can do, is take advantage of callbacks.
protocol KeyboardHandler {
func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void))
}
extension KeyboardHandler {
func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void)) {
let toolbar = UIToolbar()
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: self,
action: nil
callback(doneButton)
}
}
Then, add an extension for the class you want to implement your protocol
extension ViewController: KeyboardHandler {
func addToolbar(textField: UITextField) {
addToolbar(textField: textField) { doneButton in
doneButton.action = #selector(self.donePressed)
}
}
#objc func donePressed() {
self.view.endEditing(true)
}
}
Instead of setting the action on creation, set it just after creation in the callback.
This way, you still get your desired functionality and can call the function in your class (ex. ViewController) without even seeing the callbacks!
I have made another attempt, from another point of view. I use in many of my developments, a protocol to handle the style of UINavigationBar in a global way, from each of the UIViewController contained in it.
One of the biggest problems of doing this is the standard behavior to return to the previous UIViewController (pop) and dismiss a UIViewController shown in a modal way. Let's look at some code:
public protocol NavigationControllerCustomizable {
}
extension NavigationControllerCustomizable where Self: UIViewController {
public func setCustomBackButton(on navigationItem: UINavigationItem) {
let backButton = UIButton()
backButton.setImage(UIImage(named: "navigationBackIcon"), for: .normal)
backButton.tintColor = navigationController?.navigationBar.tintColor
backButton.addTarget(self, action: #selector(defaultPop), for: .touchUpInside)
let barButton = UIBarButtonItem(customView: backButton)
navigationItem.leftBarButtonItem = barButton
}
}
This is a very simplified (and slightly modified) version of the original protocol, although it will be worth explaining the example.
As you can see, a #selector is being set within a protocol extension. As we know, protocol extensions are not exposed to Objective-C and therefore this will generate an error.
My solution is to wrap the methods that handle the standard behaviors of all my UIViewController (pop and dismiss) in another protocol and extend UIViewController to it. Viewing this in code:
public protocol NavigationControllerDefaultNavigable {
func defaultDismiss()
func defaultPop()
}
extension UIViewController: NavigationControllerDefaultNavigable {
public func defaultDismiss() {
dismiss(animated: true, completion: nil)
}
public func defaultPop() {
navigationController?.popViewController(animated: true)
}
}
With this workaround, all UIViewController implementing the NavigationControllerCustomizable will immediately have the methods defined in NavigationControllerDefaultNavigable, with their default implementation, and therefore be accessible from Objective-C to create expressions of type #selector, without any type of error.
I hope this explanation can help someone.
Here's my idea: avoid mixing swift protocol & objc protocol.
#Frédéric Adda answer have the downside that you are responsible to unregister your observer, because it uses the block based way of adding an observer. In iOS 9 and later, the 'normal' way of adding an observer, will hold a weak reference to the observer and therefore the developer doesn't have to unregister the observer.
The following way will use the 'normal' way of adding an observer through protocol extensions. It uses a bridging class that will hold the selector.
Pro's:
You do not have the manually remove the observer
Typesafe way of using the NotificationCenter
Con's:
You have to call register manually. Do this once after self is fully initialized.
Code:
/// Not really the user info from the notification center, but this is what we want 99% of the cases anyway.
public typealias NotificationCenterUserInfo = [String: Any]
/// The generic object that will be used for sending and retrieving objects through the notification center.
public protocol NotificationCenterUserInfoMapper {
static func mapFrom(userInfo: NotificationCenterUserInfo) -> Self
func map() -> NotificationCenterUserInfo
}
/// The object that will be used to listen for notification center incoming posts.
public protocol NotificationCenterObserver: class {
/// The generic object for sending and retrieving objects through the notification center.
associatedtype T: NotificationCenterUserInfoMapper
/// For type safety, only one notification name is allowed.
/// Best way is to implement this as a let constant.
static var notificationName: Notification.Name { get }
/// The selector executor that will be used as a bridge for Objc - C compability.
var selectorExecutor: NotificationCenterSelectorExecutor! { get set }
/// Required implementing method when the notification did send a message.
func retrieved(observer: T)
}
public extension NotificationCenterObserver {
/// This has to be called exactly once. Best practise: right after 'self' is fully initialized.
func register() {
assert(selectorExecutor == nil, "You called twice the register method. This is illegal.")
selectorExecutor = NotificationCenterSelectorExecutor(execute: retrieved)
NotificationCenter.default.addObserver(selectorExecutor, selector: #selector(selectorExecutor.hit), name: Self.notificationName, object: nil)
}
/// Retrieved non type safe information from the notification center.
/// Making a type safe object from the user info.
func retrieved(userInfo: NotificationCenterUserInfo) {
retrieved(observer: T.mapFrom(userInfo: userInfo))
}
/// Post the observer to the notification center.
func post(observer: T) {
NotificationCenter.default.post(name: Self.notificationName, object: nil, userInfo: observer.map())
}
}
/// Bridge for using Objc - C methods inside a protocol extension.
public class NotificationCenterSelectorExecutor {
/// The method that will be called when the notification center did send a message.
private let execute: ((_ userInfo: NotificationCenterUserInfo) -> ())
public init(execute: #escaping ((_ userInfo: NotificationCenterUserInfo) -> ())) {
self.execute = execute
}
/// The notification did send a message. Forwarding to the protocol method again.
#objc fileprivate func hit(_ notification: Notification) {
execute(notification.userInfo! as! NotificationCenterUserInfo)
}
}
From my GitHub (you can't use the code through Cocoapods): https://github.com/Jasperav/JVGenericNotificationCenter
Here is a similar use-case, you can call a method through a selector without using #objc as in swift by using the dynamic keyword. By doing so, you are instructing the compiler to use dynamic dispatch implicitly.
import UIKit
protocol Refreshable: class {
dynamic func refreshTableData()
var tableView: UITableView! {get set}
}
extension Refreshable where Self: UIViewController {
func addRefreshControl() {
tableView.insertSubview(refreshControl, at: 0)
}
var refreshControl: UIRefreshControl {
get {
let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
if let control = _refreshControl[tmpAddress] as? UIRefreshControl {
return control
} else {
let control = UIRefreshControl()
control.addTarget(self, action: Selector(("refreshTableData")), for: .valueChanged)
_refreshControl[tmpAddress] = control
return control
}
}
}
}
fileprivate var _refreshControl = [String: AnyObject]()
class ViewController: UIViewController: Refreshable {
#IBOutlet weak var tableView: UITableView! {
didSet {
addRefreshControl()
}
}
func refreshTableData() {
// Perform some stuff
}
}
This code does not compile and might sound stupid as it is, but i'll explain why it's so important!
#objc protocol p {
optional func f1()
func f2()
}
extension p {
func f1() { }
func f2() { }
}
class foo: p {
}
Compiler says Type c does not conform to protocol 'p' and that's maybe because you can not use #objc optional and extensions at the same time (and does not make sence in this scenario either). But consider the following example:
I want to set a selector on a non-optional method defined in protocol in my extension (main reason i used #objc):
func f1() { } -> func f1() { ... #selector(Self.f2) ... }
And i also want my f2() function to have default behaviour. If i mark f2() as optional, it can not be used in #selector because compiler does not know if this method actually exists in the case of need. Sure there're lots of nasty workarounds like global methods, sending Selectors to methods as input and etc, but is there a clean way to achieve it?
This is the practical issue
#objc
protocol Refreshable {
weak var refreshControl: UIRefreshControl? { get set }
optional func setupRefreshControl()
func refresh()
}
#objc
protocol ContentLoader {
func load(reset: Bool)
}
extension Refreshable where Self: ContentLoader {
func refresh() {
delay(0.75) { [weak self] in
self?.load(true)
}
}
}
extension Refreshable where Self: UICollectionViewController {
func setupRefreshControl() {
let newRefreshControl = UIRefreshControl()
newRefreshControl.tintColor = UIColor.grayColor()
newRefreshControl.addTarget(self, action: #selector(Self.refresh), forControlEvents: .ValueChanged)
collectionView?.addSubview(newRefreshControl)
refreshControl = newRefreshControl
}
}
Now if a ViewController implements Refreshable and ContentLoader, it does not find the default refresh function, but it does find setupRefreshControl. So i figured let's mark refresh as optional too, but by doing that, you can not send it to selector any more.
I even tried this:
func refresh() -> optional func refresh()
and
let str = "refresh"
let sel = Selector(str)
It silents the compiler yes, but does not work either... rises unrecognized selector sent to instance....
I think this is not possible in swift (because of the way it bridges to #objc protocols). But this is a work around(using Obj-c associated objects) to solve the unrecognized selector sent to instance... problem.
fileprivate class AssociatedObject: NSObject {
var closure: (() -> ())? = nil
func trigger() {
closure?()
}
}
// Keys should be global variables, do not use, static variables inside classes or structs.
private var associatedObjectKey = "storedObject"
protocol CustomProtocol: class {
func setup()
}
extension CustomProtocol where Self: NSObject {
fileprivate var associatedObject: AssociatedObject? {
get {
return objc_getAssociatedObject(self, &associatedObjectKey) as? AssociatedObject
}
set {
objc_setAssociatedObject(self, &associatedObjectKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func setup() {
let object = AssociatedObject()
object.closure = { [weak self] in // Do not forget to use weak in order to avoid retain-cycle
self?.functionToCallIndirectlyWithSelector()
}
let selector = #selector(object.trigger)
// Uncomment next line to test it's functionality
object.perform(selector)
// Here, you must add selector to the target which needs to call the selector, for example:
// refreshControl.addTarget(object, action: selector, forControlEvents: .valueChanged)
self.associatedObject = object
}
func functionToCallIndirectlyWithSelector() {
print("Function got called indirectly.")
}
}
class CustomClass: NSObject, CustomProtocol {}
let instance = CustomClass()
instance.setup()
I added Self: NSObject constraint to be able to test it's functionality in playground, I'm not sure if it's necessary or not.
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.