In the creation of a swift iOS app, I needed to handle the event of a UIButton press outside of the parent view controller, so I created a (very simple) protocol to delegate that responsibility to a different class:
import UIKit
protocol MyButtonProtocol {
func buttonPressed(sender: UIButton)
}
However, when I try to addTarget to a UIButton with that protocol, I get this error: Cannot convert value of type 'MyButtonProtocol' to expected argument type 'AnyObject?'. Shouldn't anything be able to be converted to AnyObject?? Here is my main code:
import UIKit
class MyView: UIView {
var delegate: MyButtonProtocol
var button: UIButton
init(delegate: MyButtonProtocol) {
self.delegate = delegate
button = UIButton()
//... formatting ...
super.init(frame: CGRect())
button.addTarget(delegate, action: "buttonPressed:", forControlEvents: .TouchUpInside)
addSubview(button)
//... more formatting ...
}
}
Thanks in advance.
AnyObject is the protocol to which all classes conform.
To define a protocol which can only adopted by classes, add
: class to the definition:
protocol MyButtonProtocol : class {
func buttonPressed(sender: UIButton)
}
Without that modification,
var delegate: MyButtonProtocol
can be a struct or enum type and that is not convertible to AnyObject.
//i hope it will work
import UIKit
class MyView: UIView {
var delegate: MyButtonProtocol
var button: UIButton
init(delegate: MyButtonProtocol) {
self.delegate = delegate
button = UIButton()
//... formatting ...
super.init(frame: CGRect())
button.addTarget(delegate, action: Selector("buttonPressed:") forControlEvents: .TouchUpInside)
addSubview(button)
//... more formatting ...
}
}
Related
I am trying to write a class that has a method which observe text changes on UITextField objects.
When in ViewController, code below works as intended:
class ViewController: UIViewController {
#IBOutlet weak var password: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(view, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
#objc func textFieldDidChange(_ textField: UITextField) {
print(textField.text!)
}
}
So i wrote a class and put methods in it as below:
internal class ListenerModule: NSObject, UITextFieldDelegate {
internal func textWatcher(textField: UITextField!, view: UIViewController!) {
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
}
#objc internal func textFieldDidChange(_ textField: UITextField) {
print(textField.text!)
}
}
//And in ViewController,
...
ListenerModule().textWatcher(textField: password, view: self)
...
But it does not work.
How can i add target to a TextField in a class or a library?
I think it could be because you are not persisting with your ListenerModule object.
I believe you are doing this ListenerModule().textWatcher(textField: password, view: self) in some function so the scope of the object created is limited to that function.
You could do the following:
// Globally in your UIViewController subclass
var listenerModule: ListenerModule?
// Some set up function after text field is initialized
private func setup()
{
listenerModule = ListenerModule()
// Then call the text watcher, not sure why you pass the view,
// doesn't seem like you use it
listenerModule?.textWatcher(textField: password, view: self)
}
Give this a try and see if this solves your issue
Is there any way to use addTarget on something other than self (which seems to be the most common use case)?
Yes, you can use a target other than self. The most common use is to call addTarget with self where self is a reference to the viewController that the adds the UIControl to its view hierarchy. But you aren't required to use it that way. The target is merely a reference to an object, so you can pass it a reference to any object you want. The action is a Selector which needs to be defined as an instance method on the class of that object, and that method must be available to Objective-C (marked with #objc or #IBAction) and it must take either no parameters, just the sender, or the sender and the event.
You can also pass nil as the target, which tells iOS to search up the responder chain for the action method.
Here's a little standalone example:
import UIKit
class Foo {
#objc func buttonClicked() {
print("clicked")
}
}
class ViewController: UIViewController {
let foo = Foo()
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 200, width: 100, height: 30))
button.setTitle("Press me", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(foo, action: #selector(Foo.buttonClicked), for: .touchUpInside)
self.view.addSubview(button)
}
}
You can certainly set up some other object to receive control actions. Consider the following view controller:
First, define a class who's job is to respond to button tap actions:
#objc class ButtonTarget: NSObject {
#IBAction func buttonAction(_ sender: Any) {
print("In \(#function)")
}
}
Now define a view controller that creates a ButtonTarget object
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
lazy var buttonTarget = ButtonTarget() //Give the ViewController a `ButtonTarget`
override func viewDidLoad() {
super.viewDidLoad()
//Add a taret/action to the button that invokes the method `buttonAction(_:)`
button.addTarget(
buttonTarget,
action: #selector(ButtonTarget.buttonAction(_:)),
for: .touchUpInside)
}
}
Consider the following code, which prints "working" when a button is pressed:
protocol MyClassDelegate: class {
func foo()
}
class MyClass {
weak var delegate: MyClassDelegate?
func foo() {
delegate?.foo()
}
let button: UIButton = {
button.addTarget(self, action: #selector(foo), for: .touchUpInside)
return button
}()
}
class MyViewController { ... }
extension MyViewController: MyClassDelegate {
func foo() {
print("working")
}
}
When I try adding a parameter to MyClassDelegate's foo method, "working" stops printing (meaning the button stops working?). I.e.:
protocol MyClassDelegate: class {
func foo(_ str: String)
}
class MyClass {
weak var delegate: MyClassDelegate?
func foo() {
delegate?.foo("working")
}
let button: UIButton = {
button.addTarget(self, action: #selector(foo), for: .touchUpInside)
return button
}()
}
class MyViewController { ... }
extension MyViewController: MyClassDelegate {
func foo(_ str: String) {
print(str)
}
}
How can I get the second version of the code with the parameter to work? Thanks.
You are calling a wrong method on the delegate . Your MyClassDelegate doesn't have method named showDetails() . Call your delegate method this way:
func foo() {
delegate?.foo("working")
}
The problem was that button needs to be a declared with lazy var rather than with let.
Did you do that: cell.delegate = self in your ViewController?
I've got a protocol extension it used to work perfectly before swift 2.2.
Now I have a warning that tells me to use the new #selector, but if I add it
no method declared with Objective-C Selector.
I tried to reproduce the issue in this few lines of code, that can be easily copy and paste also into playground
protocol Tappable {
func addTapGestureRecognizer()
func tapGestureDetected(gesture:UITapGestureRecognizer)
}
extension Tappable where Self: UIView {
func addTapGestureRecognizer() {
let gesture = UITapGestureRecognizer(target: self, action:#selector(Tappable.tapGestureDetected(_:)))
addGestureRecognizer(gesture)
}
}
class TapView: UIView, Tappable {
func tapGestureDetected(gesture:UITapGestureRecognizer) {
print("Tapped")
}
}
There is also a suggestion to append to that method in the protocol #objc, but if I do it asks me also to add it to the class that implements it, but once I add the class doesn't conform to the protocol anymore, because it doesn't seems to see the implementation in the protocol extension.
How can I implement this correctly?
I had a similar problem. here is what I did.
Marked the protocol as #objc.
Marked any methods I extended with a default behavior as optional.
Then used Self. in the #selector.
#objc public protocol UpdatableUserInterfaceType {
optional func startUpdateUITimer()
optional var updateInterval: NSTimeInterval { get }
func updateUI(notif: NSTimer)
}
public extension UpdatableUserInterfaceType where Self: ViewController {
var updateUITimer: NSTimer {
return NSTimer.scheduledTimerWithTimeInterval(updateInterval, target: self, selector: #selector(Self.updateUI(_:)), userInfo: nil, repeats: true)
}
func startUpdateUITimer() {
print(updateUITimer)
}
var updateInterval: NSTimeInterval {
return 60.0
}
}
You can create a property which is a Selector... Example:
protocol Tappable {
var selector: Selector { get }
func addTapGestureRecognizer()
}
extension Tappable where Self: UIView {
func addTapGestureRecognizer() {
let gesture = UITapGestureRecognizer(target: self, action: selector)
addGestureRecognizer(gesture)
}
}
class TapView: UIView, Tappable {
var selector = #selector(TapView.tapGestureDetected(_:))
func tapGestureDetected(gesture:UITapGestureRecognizer) {
print("Tapped")
}
}
The error stops to show and it is not more necessary to set your protocol and class with the #objc decorator.
This solution is not the most elegant, but looks ok until now.
This answer is quite similar to Bruno Hecktheuers, but instead of having everyone that wants to conform to the "Tappable" protocol implement the variable "selector", we choose to pass it as a parameter to the addTapGestureRecognizer function:
protocol Tappable {
func addTapGestureRecognizer(selector selector: Selector)
func tapGestureDetected(gesture:UITapGestureRecognizer)
}
extension Tappable where Self: UIView {
func addTapGestureRecognizer(selector selector: Selector)
let gesture = UITapGestureRecognizer(target: self, action: selector)
addGestureRecognizer(gesture)
}
}
class TapView: UIView, Tappable {
func tapGestureDetected(gesture:UITapGestureRecognizer) {
print("Tapped")
}
}
and then just pass the selector wherever it is used:
addTapGestureRecognizer(selector: #selector(self.tapGestureDetected(_:)))
This way we avoid having the ones implementing this protocol having to implement the selector variable and we also avoid having to mark everyone using this protocol with "#objc". Feels like this approach is less bloated.
Here is a working example using Swift 3. It uses a standard Swift protocol without the need for any #objc decorations and a private extension to define the callback function.
protocol PlayButtonPlayable {
// be sure to call addPlayButtonRecognizer from viewDidLoad or later in the display cycle
func addPlayButtonRecognizer()
func handlePlayButton(_ sender: UITapGestureRecognizer)
}
fileprivate extension UIViewController {
#objc func _handlePlayButton(_ sender: UITapGestureRecognizer) {
if let playable = self as? PlayButtonPlayable {
playable.handlePlayButton(sender)
}
}
}
fileprivate extension Selector {
static let playTapped =
#selector(UIViewController._handlePlayButton(_:))
}
extension PlayButtonPlayable where Self: UIViewController {
func addPlayButtonRecognizer() {
let playButtonRecognizer = UITapGestureRecognizer(target: self, action: .playTapped)
playButtonRecognizer.allowedPressTypes = [ NSNumber(value: UIPressType.playPause.rawValue as Int) ]
view.addGestureRecognizer(playButtonRecognizer)
}
}
I happened to see this in the side bar, I recently had this same issue.. Unfortunately, due to Objective-C runtime limitations you cannot use #objc on protocol extensions, I believe this issue was closed early this year.
The issue arises because the extension is added after the conformance of the protocol, therefor there is no way to guarantee that conformance to the protocol is met. That said, it is possible to call a method as a selector from anything that subclasses NSObject and conforms to the protocol. This is most often done with delegation.
This implies you could create an empty wrapper subclass that conforms to the protocol and use the wrapper to call its methods from the protocol that are defined in the wrapper, any other undefined methods from the protocol can be passed to the delegate. There are other similar solutions that use a private extension of a concrete class such as UIViewController and define a method that calls the protocol method but these are also tied to a particular class and not a default implementation of a particular class that happens to conform to the protocol.
Realize that you are trying to implement a default implementation of a protocol function that uses another of it's own protocol functions to define a value for it's own implementation. whew!
Protocol:
public protocol CustomViewDelegate {
func update()
func nonDelegatedMethod()
}
View:
Use a delegate, and define a wrapper method to safely unwrap the delegate’s method.
class CustomView: UIView {
let updateButton: UIButton = {
let button = UIButton(frame: CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 150, height: 50)))
button.backgroundColor = UIColor.lightGray
button.addTarget(self, action: #selector(doDelegateMethod), for: .touchUpInside)
return button
}()
var delegate:CustomViewDelegate?
required init?(coder aDecoder: NSCoder) {
fatalError("Pew pew, Aghh!")
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(updateButton)
}
#objc func doDelegateMethod() {
if delegate != nil {
delegate!.update()
} else {
print("Gottfried: I wanted to be a brain surgeon, but I had a bad habit of dropping things")
}
}
}
ViewController:
Conform the View Controller to the view’s delegate: and implement the protocol’s method.
class ViewController: UIViewController, CustomViewDelegate {
let customView = CustomView(frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200)))
override func viewDidLoad() {
super.viewDidLoad()
customView.backgroundColor = UIColor.red
customView.delegate = self //if delegate is not set, the app will not crash
self.view.addSubview(customView)
}
// Protocol -> UIView Button Action -> View Controller's Method
func update() {
print("Delegating work from View that Conforms to CustomViewDelegate to View Controller")
}
//Protocol > View Controller's Required Implementation
func nonDelegatedMethod() {
//Do something else
}
}
Note that the view controller only had to conform to the delegate and did not set the selector of some property of the view, this separates the view (and it's protocol) from view controller.
You already have a UIView named TapView that inherits from UIView and Tappable so your implementation could be:
Protocol:
protocol TappableViewDelegate {
func tapGestureDetected(gesture:UITapGestureRecognizer)
}
TappableView:
class TappableView: UIView {
var delegate:TappableViewDelegate?
required init?(coder aDecoder: NSCoder) {
fatalError("Pew pew, Aghh!")
}
override init(frame: CGRect) {
super.init(frame: frame)
let gesture = UITapGestureRecognizer(target: self, action: #selector(doDelegateMethod(gesture:)))
addGestureRecognizer(gesture)
}
#objc func doDelegateMethod(gesture:UITapGestureRecognizer) {
if delegate != nil {
delegate!.tapGestureDetected(gesture: gesture)
} else {
print("Gottfried: I wanted to be a brain surgeon, but I had a bad habit of dropping things")
}
}
}
ViewController:
class ViewController: UIViewController, TappableViewDelegate {
let tapView = TappableView(frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200)))
override func viewDidLoad() {
super.viewDidLoad()
tapView.backgroundColor = UIColor.red
tapView.delegate = self
self.view.addSubview(tapView)
}
func tapGestureDetected(gesture: UITapGestureRecognizer) {
print("User did tap")
}
}
I have a set of view controllers which will have a Menu bar button. I created a protocol for those viewControllers to adopt. Also, I've extended the protocol to add default functionalities.
My protocol looks like,
protocol CenterViewControllerProtocol: class {
var containerDelegate: ContainerViewControllerProtocol? { get set }
func setupMenuBarButton()
}
And, the extension looks like so,
extension CenterViewControllerProtocol where Self: UIViewController {
func setupMenuBarButton() {
let barButton = UIBarButtonItem(title: "Menu", style: .Done, target: self, action: "menuTapped")
navigationItem.leftBarButtonItem = barButton
}
func menuTapped() {
containerDelegate?.toggleSideMenu()
}
}
My viewController adopts the protocol -
class MapViewController: UIViewController, CenterViewControllerProtocol {
weak var containerDelegate: ContainerViewControllerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBarButton()
}
}
I got the button to display nicely, but when I click on it, the app crashes with
[AppName.MapViewController menuTapped]: unrecognized selector sent to instance 0x7fb8fb6ae650
If I implement the method inside the ViewController, it works fine. But I'd be duplicating the code in all viewControllers which conform to the protocol.
Anything I'm doing wrong here?
Thanks in advance.
It seems like using protocol extensions are not supported at this point in time. According to fluidsonic's answer here:
In any case all functions you intend to use via selector should be marked with dynamic or #objc. If this results in an error that #objc cannot be used in this context, then what you are trying to do is simply not supported."
In your example, I think one way around this would be to create a subclass of UIBarButtonItem that calls a block whenever it is tapped. Then you could call containerDelegate?.toggleSideMenu() inside that block.
This compiles but crash also in Xcode7.3 Beta so finally you should use a ugly super class as target of the action, that i suppose that it's what you and me are trying to avoid.
This is an old question but I also ran into the same issue and came up with a solution which may not be perfect but it's the only way I could think of.
Apparently even in Swift 3, it's not possible to set a target-action to your protocol extension. But you can achieve the desired functionality without implementing your func menuTapped() method in all your ViewControllers that conforms to your protocol.
first let's add new methods to your protocol
protocol CenterViewControllerProtocol: class {
var containerDelegate: ContainerViewControllerProtocol? { get set }
//implemented in extension
func setupMenuBarButton()
func menuTapped()
//must implement in your VC
func menuTappedInVC()
}
Now change your extention like this
extension CenterViewControllerProtocol where Self: UIViewController {
func setupMenuBarButton() {
let barButton = UIBarButtonItem(title: "Menu", style: .Done, target: self, action: "menuTappedInVC")
navigationItem.leftBarButtonItem = barButton
}
func menuTapped() {
containerDelegate?.toggleSideMenu()
}
}
Notice now button's action is "menuTappedInVC" in your extension, not "menuTapped" . And every ViewController that conforms to CenterViewControllerProtocol must implement this method.
In your ViewController,
class MapViewController: UIViewController, CenterViewControllerProtocol {
weak var containerDelegate: ContainerViewControllerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBarButton()
}
func menuTappedInVC()
{
self.menuTapped()
}
All you have to do is implement menuTappedInVC() method in your VC and that will be your target-action method. Within that you can delegate that task back tomenuTapped which is already implemented in your protocol extension.
I think you can wrap Target-Action to make Closure from them and then use it in similar way I have used Target-Action for UIGestureRecognizer
protocol SomeProtocol {
func addTouchDetection(for view: UIView)
}
extension SomeProtocol {
func addTouchDetection(for view: UIView) {
let tapGestureRecognizer = UITapGestureRecognizer(callback: { recognizer in
// recognizer.view
})
view.addGestureRecognizer(tapGestureRecognizer)
}
}
// MARK: - IMPORTAN EXTENSION TO ENABLE HANDLING GESTURE RECOGNIZER TARGET-ACTIONS AS CALLBACKS
extension UIGestureRecognizer {
public convenience init(callback: #escaping (_ recognizer: UIGestureRecognizer) -> ()) {
let wrapper = CallbackWrapper(callback)
self.init(target: wrapper, action: #selector(CallbackWrapper.callCallback(_:)))
// retaint callback wrapper
let key = UnsafeMutablePointer<Int8>.allocate(capacity: 1);
objc_setAssociatedObject(self, key, wrapper, .OBJC_ASSOCIATION_RETAIN)
}
class CallbackWrapper {
var callback : (_ recognizer: UIGestureRecognizer) -> ();
init(_ callback: #escaping (_ recognizer: UIGestureRecognizer) -> ()) {
self.callback = callback;
}
#objc public func callCallback(_ recognizer: UIGestureRecognizer) {
self.callback(recognizer);
}
}
}