2

I have the following Swift code.

extension UIImageView {
    func enableClickablePrint() {
        let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
        self.addGestureRecognizer(imageTap)
        self.isUserInteractionEnabled = true
    }
    func disableClickablePrint() {
        // HERE
    }
    func toggleClickablePrint() {
        // HERE
    }

    @objc fileprivate func imageTapped(_ sender: UITapGestureRecognizer) {
        print("Image tapped")
    }
}

The problem I'm running into is how to fill out the disableClickablePrint and toggleClickablePrint functions.

I'd like to be able to do something like the following.

extension UIImageView {
    var imageTap: UITapGestureRecognizer?
    func enableClickablePrint() {
        imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
        self.addGestureRecognizer(imageTap)
        self.isUserInteractionEnabled = true
    }
    func disableClickablePrint() {
        if let theImageTap = imageTap {
            self.removeGestureRecognizer(theImageTap)
            imageTap = nil
        }
    }
    func toggleClickablePrint() {
        if let theImageTap = imageTap {
            disableClickablePrint()
        } else {
            enableClickablePrint()
        }
    }

    @objc fileprivate func imageTapped(_ sender: UITapGestureRecognizer) {
        print("Image tapped")
    }
}

But of course the problem is you can't store properties in extensions like I'm wanting to do.

Anyway to achieve this? I want to try to keep this as clean as possible, without resorting to fancy tricks unless absolutely required.

Would the correct thing to do to be to convert this into a subclass of UIImageView? I'd like to try to avoid that if possible just because if I want to turn this into a framework or library or something, subclasses don't integrate as nicely into interface builder and the developer would have to add the extra step of changing the class of all their image views. Which I think would be awesome to avoid if possible.

vacawama
  • 150,663
  • 30
  • 266
  • 294
Charlie Fish
  • 18,491
  • 19
  • 86
  • 179
  • You can try `gestureRecognizers` and take the one you added and remove it with remove. – Fabian Aug 18 '18 at 18:57
  • @Purpose What if there are multiple `gestureRecognizers` that aren't all created by my code tho? – Charlie Fish Aug 18 '18 at 18:59
  • 1
    Why not subclass `UIImageView` instead of trying this with an extension? – rmaddy Aug 18 '18 at 19:05
  • @rmaddy Maybe that is the best solution. I just thought it would make more sense in an extension so you don't have to worry about a separate class and it would all be integrated right into the existing image. – Charlie Fish Aug 18 '18 at 19:08
  • @rmaddy I also thought that if I converted this into a library or framework, and a developer wanted to have an image that inherits this superclass, and another superclass from either another part of their code or another library, that won't work since you can only have one parent class in Swift and can't inherit from multiple parent classes. – Charlie Fish Aug 18 '18 at 19:10
  • If only there was something like `tag` for `UIGestureRecognizer`. _sigh_ :( – Rakesha Shastri Aug 18 '18 at 19:26

4 Answers4

3

Your problem is that you need to be able to recognize the UIGestureRecognizer you added, but you can't store it in a property.

Here's a (tested) solution that subclasses UITapGestureRecognizer to make the UIGestureRecognizer identifiable and then searches self.gestureRecognizers with first(where:) to see if one has been added:

extension UIImageView {

    class _CFTapGestureRecognizer : UITapGestureRecognizer { }

    private var _imageTap: _CFTapGestureRecognizer? { return self.gestureRecognizers?.first(where: { $0 is _CFTapGestureRecognizer }) as? _CFTapGestureRecognizer }

    func enableClickablePrint() {
        // Only enable once
        if _imageTap == nil {
            let imageTap = _CFTapGestureRecognizer(target: self, action: #selector(imageTapped))
            self.addGestureRecognizer(imageTap)
            self.isUserInteractionEnabled = true
        }
    }

    func disableClickablePrint() {
        if let theImageTap = _imageTap {
            self.removeGestureRecognizer(theImageTap)
        }
    }

    func toggleClickablePrint() {
        if _imageTap == nil {
            enableClickablePrint()
        } else {
            disableClickablePrint()
        }
    }

    @objc fileprivate func imageTapped(_ sender: UITapGestureRecognizer) {
        print("Image tapped")
    }
}
vacawama
  • 150,663
  • 30
  • 266
  • 294
0

I've use this extension for years and it work not only image but with any view.

extension UIView {
  func addTapGuesture(target: Any, action: Selector) {
    let tap = UITapGestureRecognizer(target: target, action: action)
    tap.numberOfTapsRequired = 1
    addGestureRecognizer(tap)
    isUserInteractionEnabled = true
  }
}

Usage:

imageView.addTapGuesture(target: self, action: #selector(imageTapped))

@objc func imageTapped() {
   print("Tapped")
}
-1

You could make an extension variable using a getter/setter:

extension UIImageView {

var tap: UITapGestureRecognizer {
    get {
        return //the created tap
    }
    set(value) {
        print(value)
    }
}

}
Pranav Wadhwa
  • 7,666
  • 6
  • 39
  • 61
-1

Since Extensions in swift can't contain stored properties, these are the solutions:

Fisrt one is the most used workaround is to use Objective_C runtime, by using objc_getAssociatedObject and objc_setAssociatedObject functions.

OK it's a nice solution, but if there is pure swift approach to do that so why not!

In your extension, define a struct with the field that you want to use, here you want for example UITapGestureRecognizer

Tip Create this struct as private You don't need anyone to access it of course.

Then define a computed property that will use this struct....

By doing this you have achieved what you need and you don't use the Objective-C at all .

Example :

extension UIImageView {
    private struct TapGestureHelper{
        static var tapGestureRecognizer : UITapGestureRecognizer?
    }

    var imageTap: UITapGestureRecognizer?{
        get {
            return TapGestureHelper.tapGestureRecognizer
        }
        set {
            TapGestureHelper.tapGestureRecognizer = newValue
        }
    }

    func enableClickablePrint() {
        imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
        self.addGestureRecognizer(imageTap!)
        self.isUserInteractionEnabled = true
    }

    func disableClickablePrint() {
          guard let gesture = self.gestureRecognizers?.first else {
              return
          }
          self.removeGestureRecognizer(gesture)
          self.imageTap = nil
    }

    func toggleClickablePrint() {
        if let theImageTap = imageTap {
            disableClickablePrint()
        } else {
            enableClickablePrint()
        }
    }

    @objc fileprivate func imageTapped(_ sender: UITapGestureRecognizer) {
        print("Image tapped")
    }
}
mojtaba al moussawi
  • 1,360
  • 1
  • 13
  • 21
  • My only question about this solution is because the `tapGestureRecognizer` is a static variable, will this cause problems if there are multiple UIImageViews? – Charlie Fish Aug 18 '18 at 19:33
  • @Purpose @CharlieFish Isn't that just for dummy initialization? Isn't the tap immediately changed to another instance inside `func enableClickablePrint()` ? – Rakesha Shastri Aug 18 '18 at 19:35
  • @RakeshaShastri Any `UIImageView` is sharing the same storage, so if two are enabled after each other, disable would only remove the gesture recognizer for the second, same as if it were a static var. – Fabian Aug 18 '18 at 19:37
  • @Purpose you would only disable if you enable. And if you are disabling without enabling, I don't think the intended tap gesture has been added anyway. So would it matter? – Rakesha Shastri Aug 18 '18 at 19:39
  • 1
    @RakeshaShastri You are right in that UIImageViews are not sharing the same gesture recognizer with it, deleting though doesn’t work correctly since there can be many image views and the storage gets overriden with the second so it can’t disable the first again. – Fabian Aug 18 '18 at 19:42
  • I have updated my answer, and it works even if there was more than one image. – mojtaba al moussawi Aug 18 '18 at 20:02
  • @mojtabaalmoussawi How do you know the first gesture recognizer is the one that was added? There may be several attached to the `UIImageView`. This leads to vacawama’s answer again. – Fabian Aug 18 '18 at 20:43
  • First of all please stop deleting and re commenting!!!! Ok first of all the UImageViews will not share the same gesture, i don't know from the beginning of this discussion why this was considered , since the tapGesture can't be assigned to more than one view at a time please check the documentation.. Also you have commented previously that it's not working without even testing it , how did you know?.. And as @Rakesha Shastri mentioned it's just for dummy initialization every time enableClicablePrint() called a new instance will be created. – mojtaba al moussawi Aug 18 '18 at 21:24
  • And For your comment that how did i know that it will be the first not others, there's no otherssss!! The array will hold one gesture ( of course if enableClicablePrint called twice before disabling the first). so i took the first one which is the only one, obvious! Finally the idea behind the answer is how to workaround when you need to have stored properties inside extensions,Thx for your comments anyway. gd luck! – mojtaba al moussawi Aug 18 '18 at 21:25
  • @mojtabaalmoussawi Offtopic: If you want sb who already wrote comment to get a notification when you answer you have to add @name to the beginning of the comment. Anyway `commented that it’s not working properly, how did you know?`: By looking at how your solution works. Having the possibility that there are more gesture recognizers enables interoperability by allowing others to add gesture recognizers too. The static property is still not stored, but shared for the whole type, so only one gesture recognizer can live inside this shared storage space, so only the latest assigned can be removed. – Fabian Aug 18 '18 at 22:10
  • This is of course a comment on the static var way, so irrelevant now since it got replaced with the `.gestureRecognizers` way. I’m just answering so you understand what the problem with it was. – Fabian Aug 18 '18 at 22:15