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")))
Related
I'm using BulletinBoard (BLTNBoard) to create dialogs in my iOS app. There's an option to embed image inside it. I would like to extend it's functionality and allow user to manipulate this image using tap gesture. But eventually when I assign a gesture to it's imageView using addGestureRecognizer nothing happens.
Here's how I initiliaze bulletin and add gesture to the image:
class ViewController: UIViewController {
lazy var bulletinManager: BLTNItemManager = {
let rootItem: BLTNPageItem = BLTNPageItem(title: "")
return BLTNItemManager(rootItem: rootItem)
}()
override func viewDidLoad() {
//etc code
let bulletinManager: BLTNItemManager = {
let item = BLTNPageItem(title: "Welcome")
item.descriptionText = "Pleas welcome to my app"
item.actionButtonTitle = "Go"
item.alternativeButtonTitle = "Try to tap here"
item.requiresCloseButton = false
item.isDismissable = false
item.actionHandler = { item in
self.bulletinManager.dismissBulletin()
}
item.alternativeHandler = { item in
//do nothing by now
}
//
item.image = UIImage(named: "welcome")
//adding gesture to its imageView
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
return BLTNItemManager(rootItem: item)
}()
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
and nothing happens at all (no message printed in console).
However if I assign action inside alternative button it works as expected:
item.alternativeHandler = { item in
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
}
I guess the only thing which can prevent me to assign the tap event to it properly is that imageView becomes available much later than the bulletin is created (for example only when it is shown on the screen).
Could you please help and correct my code. Thanks
upd.
Ok, based on Philipp's answer I have the following solution:
class myPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
let imageView=super.imageView
imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: #selector(tapTap))
imageView?.addGestureRecognizer(tap)
return contentViews
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
When you're working with an open source library, it's easy to check out the source code to find the answer.
As you can see here, image setter doesn't initiate the image view.
Both makeContentViews makeArrangedSubviews (which are responsible for views initializing) doesn't have any finish notification callbacks.
Usually in such cases I had to fork the repo and add functionality by myself - then I'll make a pull request if I think this functionality may be needed by someone else.
But luckily for you the BLTNPageItem is marked open, so you can just subclass it. Override makeContentViews and add your logic there, something like this:
class YourOwnPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
// configure the imageView here
return contentViews
}
}
I'm trying to invoke my method from selector (action) but getting error Variable used within its own initial value Below is my code snippet
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(tapGestureRecognizer, userID)))
Below is the method which I'm calling
#objc func sendRequest(tapGestureRecognizer: UITapGestureRecognizer, identifer: String) {
print("hello world")
}
My method accepts 2 paramters. I don't know how to call it. The way I'm currently calling the sendRequestMethod is throwing error.
Please help me to get it resolved.
when those functions are inside an UIView, as GestureRecognizers usually are, then you can make a hittest and find what UIView was under your touch.
Consider this is not the ideal way to catch touched views. As every UIView also offers you a .tag you can set a numbering and use it to make assumtions to whom the hit UIView fits. See Example
class checkCheckView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(recognizer:identifer:)) )
#objc func sendRequest(recognizer:UITapGestureRecognizer, identifer:String) {
print("hello world")
// your problem will be, how is identifier passed in here?
}
let PanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panning(gesture:)))
#objc func panning(gesture:UIPanGestureRecognizer) {
//print("pan-translation %#", gesture.translation(in: self).debugDescription)
let location = gesture.location(ofTouch: 0, in: self)
//print("pan-location %#",location.debugDescription);
let hitview = self.hitTest(location, with: nil)
if hitview != nil {
let UserIDTag = hitview?.tag ?? 0
print("userId by UIView tag",UserIDTag)
}
}
}
Hint: Much easier is to subclass UIView and prepare a property that holds UserID given when allocation is done. addtarget: action:#selector() to each of them. The action/selector method gets invoked and the sender is passed in and because you know the type of the sender (YourUserUIView) you find the UserID.
With GestureRecognizer's you tend to write recognition code that can become very complex the more complex your UI will be, instead of just passing objects to actions that tell you what you are looking for.
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.
Not sure if my thinking here is correct but I have similar animations I use throughout my iOS project and I would like to condense it to 1 file and reuse wherever I want.
A brief example. In my animations file I have a scale animation
Animations.swift
class Animations {
class func scaleSmall(_ view: UIView) {
let scaleAnim = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
scaleAnim?.toValue = NSValue(cgSize: CGSize(width: 0.9, height: 0.9))
view.layer.pop_add(scaleAnim, forKey: "scaleSmallAnim")
}
}
Here I have one of my many swift files in my View folder and I would like to add that animation to the button
Button.swift
class Button: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
}
}
I thought I would be able to reference the animation from an additional file however everytime I do it this way I get the same error
Argument of '#selector' refers to instance method 'scaleSmall' that is not exposed to Objective-C
Am I referencing this function wrong?
try changing class func scaleSmall(_ view: UIView) {
to
#objc class func scaleSmall(view: UIView) {
I've confirmed my comment, so I'm posting an answer. Methods for UIButton need to be bridged to Obj-C. That's what #Kostas Tsoleridis suggests with his answer as well - it is not mixing two languages in one file, you are just marking the method for the compiler. Other solution would be to inherit from NSObject by your Animations class.
Now, as your confusion mentioned in a comment - it worked, because your Button class inherits from UIButton which is both from Obj-C world, and also inherits from NSObject down the chain.
To also address the issue mentioned in a comment under #Kostas Tsoleridis answer (and to be honest I should have thought about it before) - you can't pass self as a target and use a method from another class (even a static one). To solve this, you can use a singleton instance of your Animations class, something like this :
class Animations {
static let sharedInstance = Animations()
#objc class func scaleSmall(_ view: UIView) {
// your code
}
}
let button = UIButton()
button.addTarget(Animations.sharedInstance, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
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.