I'm trying to add a gesture recognizer on my UIImageView from my UIViewController extensions methods.
The method I want to fire when the image is tapped is a class method declared in my swift extensions :
class func openPopViewWithText(text: String!) {
print("fire!")
}
I add the selector from my extensions class too. This is how I add the selector :
infoImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(UIViewController.openPopViewWithText(_:)) ))
And this is the error that I get when I tap the ImageView :
[XXXDisplayStatsViewController openPopViewWithText:]: unrecognized selector sent to instance 0x7fd31520ada0
Complete code in my extensions class :
public extension UIViewController {
func addInfoImageViewWithText(infoText: String){
let margins : CGFloat = 30.0
let infoImageView : UIImageView = UIImageView(image: UIImage(named: "info"));
let yOrigin : CGFloat = ((self.view.y + self.view.height) - infoImageView.height) - margins
infoImageView.frame = CGRectMake(margins, yOrigin, infoImageView.width + 5, infoImageView.height + 5)
self.view.insertSubview(infoImageView, atIndex: 0)
self.view.bringSubviewToFront(infoImageView)
infoImageView.userInteractionEnabled = true
infoImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(UIViewController.openPopViewWithText(_:)) ))
}
class func openPopViewWithText(text: String!) {
print("fire!")
}
}
The problem is that first argument in your openPopViewWithText function is String rather than a UITapGestureRecognizer. When you tap on the UIImageView gesture recognizer is fired and it passes the gesture recognizer instance to the selector so you can track its properties. So what you should do is change the current function signature to this:
class func openPopViewWithText(recognizer: UITapGestureRecognizer) {
print("fire!")
}
Other than that, you should not use class functions because you're not be able to access any UIViewController properties.
Related
using swift, I'm trying to create a dynamic and generic control to reuse it, basically, the control should have a general behavior inside.
To be more specific, I have a UIScrollView and it's filled using UIViews, when you click over an UIView, the background should change.
That is working correctly.
But, for the implementation, my class of the generic control, accepts a Selector as parameter.
Both works separately, but together are not working.
The specific part of codes are:
Generic class
let clickAgendaEvent = UITapGestureRecognizer(target: self, action: #selector (self.agendaClicked (_:)))
cellSubView.addGestureRecognizer(clickAgendaEvent)
cell.addSubview(cellSubView)
let itemClickedEvent = UITapGestureRecognizer(target: viewController.self, action: self.agendaItemClicked! )
cell.addGestureRecognizer(itemClickedEvent)
And a ViewController with an implementation like this:
#objc func eventDailyAgenda(sender:UIView!){
print("Item clicked!")
}
As you can see, the second event, is not inside of the generic class, the second event is a separated implementation if the ViewController.
But, the generic class, would be implemented for other UIViewController.
Someone have an idea about how can handle it?
Look into UIGestureRecognizerDelegate to handle both gestures simultaneously. Check out the callback gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) . Return true to have both handled simultaneously.
The easier way to do it, is implementing UIGestureRecognizerDelegate.
But for my specific case didn't work, because, I have different classes implementing the behavior.
But, I found a way to do it.
And is implementing NSObject (if you are in a UIViewController you don't have to implement it.
For my case I have my class:
class MyClass:NSObject {
func createAll(){
let clickAgendaEvent = UITapGestureRecognizer(target: self, action: #selector (self.agendaClicked (_:)))
cellSubView.addGestureRecognizer(clickAgendaEvent)
cellSubView.tag = index
cell.addSubview(cellSubView)
}
#objc func agendaClicked(_ sender:AnyObject) {
baseView.setTransparentToSubViews()
print("Here")
let tap = sender as! UITapGestureRecognizer
tap.view?.backgroundColor = UIColor().hexStringToUIColor(hex: "#e0e0e0")
if let v = tap.view {
// use button
print("The tag is \(v.tag)")
}
if let c: NSObject.Type = NSClassFromString(viewController.className) as? NSObject.Type{
let c_tmp = c.init()
c_tmp.perform(Selector(("test")))
c.perform(Selector(("static_test")))
}
}
}
Add in your ViewController
#objc public func test(){
print("This is Test!!!")
}
#objc public class func static_test(){
print("This is Static Test")
}
And this extension:
import Foundation
import UIKit
extension UIViewController {
var className: String {
return NSStringFromClass(self.classForCoder)
}
}
Now, you can execute all the methods you want.
For this case, once the user touch an element, the next event is fired from the touch, and not is necessary to add a new delegate.
Maybe is not the better way, but is totally functional.
Shorter explanation:
You often want to extend on "target" ... and targets are usually Any?. But you can't have an extension on Any. How to do it?
Consider this,
extension UIViewController {
func add(tap v:UIView, _ action:Selector) {
let t = UITapGestureRecognizer(target: self, action: action)
v.addGestureRecognizer(t)
}
}
Excellent, you can now...
self.tap(redButton, #selector(clickedRedButton))
... in any view controller.
But you can do the same thing to just about any target.
So, to use the extension on a UITableViewCell say, you have to also have....
extension UIGestureRecognizerDelegate {
func add(tap v:UIView, _ action:Selector) {
let t = UITapGestureRecognizer(target: self, action: action)
v.addGestureRecognizer(t)
}
}
The target argument of UITapGestureRecognizer is actually Any?
But, you can not do this ...
extension Any {
What's the solution? How to make an extension that will work on the Any?, as for example in the first argument of UITapGestureRecognizer ?
Or as Conner'c comment suggests, is there a way to:
extension UIViewController or UIView {
rather than copying and pasting it twice?
"Any" is adhered to (passively) by every struct/class. An extension to Any would add that functionality to every single type in the language and your code. This isn't currently possible, and I doubt it ever would be (or should be).
Anyway, here are a few ways to solve this problem.
My preference is a protocol extension that adds the functionality:
protocol TapGestureAddable {
func addTapGestureRecognizer(to view: UIView, with action: Selector) -> UITapGestureRecognizer
}
extension TapGestureAddable {
func addTapGestureRecognizer(to view: UIView, with action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: self, action: action)
view.addGestureRecognizer(recognizer)
return recognizer
}
}
extension UIViewController: TapGestureAddable { }
extension UIView: TapGestureAddable { }
This forces you to knowingly choose to add the functionality to a given class, (a good thing IMO) without having to duplicate any meaningful code.
Possibly a better option would be to make this logic an extension of UIView instead:
extension UIView {
func addTapGestureRecognizer(with responder: Any, for action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: responder, action: action)
self.addGestureRecognizer(recognizer)
return recognizer
}
func addTapGestureRecognizer(with action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: self, action: action)
self.addGestureRecognizer(recognizer)
return recognizer
}
}
Otherwise, just make a global function:
func addTapGestureRecognizer(to view: UIView, with responder: Any, for action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: responder, action: action)
view.addGestureRecognizer(recognizer)
return recognizer
}
Any isn't a class in the way that NSObject is. It is merely a keyword that indicates to the Swift compiler that a variable/constant/parameter may refer to any object or struct instance, so it isn't possible to extend Any.
If you consider what you are trying to do, you would have a subtle difference between your two extensions anyway;
The UIViewController extension needs to accept a target view (your v) parameter
While, for a UIView extension, you don't need v as this will be self; it doesn't make sense to install a gesture recogniser on some other UIView.
For the UIView extension, you may want to specify a different target for the selector.
You don't add a gesture recogniser to the UIViewController, so it doesn't make, semantically, to extend UIViewController in this way.
So, to me, it seems that the logical extension looks somthing like:
extension UIView {
func add(_ action:Selector,tapHandler target:Any = self) {
let t = UITapGestureRecognizer(target: target, action: action)
self.addGestureRecognizer(t)
}
}
Now, in a UIViewController you can say something like:
self.redButton.add(Selector(("handleTap")), tapHandler: self)
While in a UIView subclass you can say:
self.add(Selector(("handleTap")))
So I wrote a protocol to make what ever UIView that conforms it draggable. However when I test this in the simulator it crashes when I try to drag the view. and displays this in the log
libc++abi.dylib: terminating with uncaught exception of type NSException
The protocol:
protocol Draggable {}
extension Draggable where Self: UIView {
func wasDragged (gestrue: UIPanGestureRecognizer) {
let translation = gestrue.translation(in: UIScreen.main.focusedView)
if let label = gestrue.view {
label.center = CGPoint(x: label.center.x + translation.x, y: label.center.y + translation.y)
}
gestrue.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: UIScreen.main, action: Selector(("wasDragged:")))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
and in a custom label class I conformed it:
class DraggableLabel: UILabel, Draggable {
}
Then I called the setGesutre function in viewDidLoad of the view controller:
override func viewDidLoad() {
super.viewDidLoad()
draggableLabel.setGesture()
}
OK I admit I don't really know what I'm doing.
The action wasDragged(gesture:) needs to be accessible for message-dispatch via the Objective-C runtime. Use the #objc annotation to make a method available for message dispatch. Methods of NSObject subclasses are automatically #objc methods.
The bad news is that this will only work for Objective-C-compatible classes or extensions. Protocol extensions like yours are not compatible, so you cannot put action methods into those extensions.
Your options are to add this functionality to a subclass or a plain class extension:
extension DraggableLabel {
func wasDragged (gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: UIScreen.main.focusedView)
center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: self,
action: #selector(wasDragged(sender:)))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
(Notice that I also changed the target of the gesture recognizer to the view instead of the main screen. Did you intend to use the responder chain to propagate the event to the right view?)
The obvious disadvantage is the reduced flexibility compared to the protocol oriented approach. If that's a problem I would look into class composition. Create a class that encapsulates the gesture recognizer and its action method. Give it a view property and configure everything when that property is set:
class DraggingBehavior: NSObject {
#IBOutlet var view: UIView? {
didSet {
guard let view = view else { return }
let gesture = UIPanGestureRecognizer(target: self, action: #selector(wasDragged(sender:)))
view.addGestureRecognizer(gesture)
view.isUserInteractionEnabled = true
}
}
func wasDragged(sender: UIGestureRecognizer) {
print("Was dragged")
// put the view translation code here.
}
}
The #IBOutlet makes this class compatible with Interface Builder. Drag in a Custom Object, set its class to DraggingBehavior, connect the view outlet to the view you would like to make draggable.
I have a subclass of UIView, in this custom view, I add a gesture recognizer during initialization.
let gameBoard = GameBoardViewController()
and
func initGestureRecognizers() {
let panRec = UIPanGestureRecognizer(target: gameBoard, action: #selector(GameBoardViewController.labelDragged(_:)))
self.addGestureRecognizer(panRec)
self.clipsToBounds = true
self.userInteractionEnabled = true
}
In the viewDidLoad of gameBoardViewController, I initialize randomNumber = 10 .
The problem i'm having is that in the labelDragged function (which I call from the selector in my UIView subclass when it is dragged ), randomNumber is Nil.
I don't have this problem when I add the gestureRecognizer from gameBoardViewController.
Can anyone tell me what i've done wrong? Thanks
I'm working on a UITableView.
Each cell has got a series of UIImageViews, and their urls are saved into an array.
When I tap on a certain image, the selector for my gesture recogniser should get both the tag of the image and the indexPath.row of the cell.
I'm currently getting the image tag from the sender parameters, and this is the way in which I'm trying to get the indexpath:
let point = sender.locationInView(self.tableView)
let indexPathRow = self.tableView.indexPathForRowAtPoint(point)?.row
However, this looks like it's not giving me the right row.
Is there an easy way to pass these two parameters to my gesture selector? Thanks.
You could subclass the gesture recogniser you're using and add the extra variables you want. Example below.
class CustomGestureRecognizer : UITapGestureRecognizer {
var url : NSURL?
// any more custom variables here
init(target: AnyObject?, action: Selector, url : NSURL) {
super.init(target: target, action: action)
self.url = url
}
}
Then when you want to get the url back out.
func didTap(sender : CustomGestureRecognizer) {
print(sender.url)
}
Simplest way I believe is to subclass the UIImageView to hold your NSIndexPath:
class MyImageView: UIImageView {
var indexpath:NSIndexPath!
}
And then to get that indexPath:
func tap(tap: UITapgestureRecognizer) {
let imgView = tap.view as! MyImageView
let indexPath = imgView.indexPath
let tag = imgView.tag
}