5

I have an image displayed in on my app that I'm creating in Swift using Xcode and I want to for example print "Image Clicked" whenever the image is tapped. Here's what I got:

This is a function that is supposed to get called whenever users tap the image:

func imageViewTapped(imageView: UIImageView) {
    print("Image Clicked")
}

I have this function that adds the user interaction with the image:

func tapRecognition(image: UIImageView) {
    image.isUserInteractionEnabled = true
    let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector(("imageViewTapped:")))
    image.addGestureRecognizer(tapRecognizer)
}

And last but not least I created an image of type UIImageView in my viewDidLoad() function and I called:

tapRecognition(image: imageView)

When I run this and click on the image, I get an error that states:

terminating with uncaught exception of type NSException.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
tee
  • 1,286
  • 2
  • 19
  • 35
  • Where is `imageView` being initialized? – John Montgomery May 24 '17 at 23:49
  • 1
    Unless you are using old Swift, you need to use the correct selector syntax and specify the correct method name in the selector. – rmaddy May 24 '17 at 23:53
  • 1
    Have you connected IBOutlet properly, this can cause to NSException – Rohit Funde May 24 '17 at 23:55
  • You could use a tapGestureRecognizer on top of the `imageView` – Johnd May 25 '17 at 00:00
  • Well I created a function that generates an image with a text in the middle of it and it returns a UIImage. I call this function in the viewDidLoad() function and I make it visible in the app by doing this: self.view.addSubview(imageView). That is all done in the viewDidLoad() function and at the end of the function i call tapRecognition. – tee May 25 '17 at 00:02
  • please refer this link and change swift syntax [Tap gesture recognization](https://stackoverflow.com/questions/38829151/swift3-ios-how-to-make-uitapgesturerecognizer-trigger-function) – Foolish May 25 '17 at 00:53

2 Answers2

13

Here's some code that will work using Swift 3:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // #1
    let myImage = UIImage(named: "myImage")
    let imageView = UIImageView()
    imageView.image = myImage

    // #2
    imageView.isUserInteractionEnabled = true
    let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
    imageView.addGestureRecognizer(tapRecognizer)

    // #3
    imageView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(imageView)
    // set imageView contraints
    
}

// #4
func imageTapped(recognizer: UITapGestureRecognizer) {
    print("Image was tapped")
    let thePoint = recognizer.location(in: view)
    let theView = recognizer.view
}
  1. While you code is a bit different and some is missing, I believe this is basically the same. Unless you need to access image or imageView elsewhere, declare them locally in viewDidLoad. (I'd suggest not naming a UIImageView as you have (image), as that can be confusing to others reading your code.)

  2. Here's where I think @rmaddy is correct. If you are using Swift 3, the correct syntax for selector is what I have. This may be the only problem with our code - and kudos for remembering that a UIImageView has a default of isUserInteractioEnabled = true.

  3. You don't have any code for the actual layout. But if you are using auto layout (as opposed to frames), the basic syntax is turn off autoresizing, make sure you add the view into the view hierarchy, and set your constraints. Many times (most even) you can set them in viewDidLoad, but if you need frame information, use either viewWillLayoutSubviews or viewDidLayoutSubviews.

  4. At first I thought your tap function was ok, but upon further inspection, it isn't. The sender isn't the view that was tapped on, it's the recognizer. I've added two lines of code after the print statement. The first puts the CGPoint of the tap into a variable. The second puts the UIView attached to the recognizer (in this case, imageView) into a variable. Also, if you need to know is a subview of imageView was in the area of the tap, use the hitTest() method.

EDIT:

I was asked how to use the view (in my example, imageView) in the function that executes on the tap.

First, the one thing you cannot do is pass it as a parameter. Both iOS and macOS doesn't work that way with their actions. But you have (at least) three options at you disposal. (None of them use the tag property, which in some scenarios is another option.)

  1. The code you've shown say you already know the the image view was tapped. This is a likely case, as every gesture you create can only be attached to a single view. If this is your scenario, consider declaring your image view at the view controller level and just code what you need inside your function.

  2. But what if you have two image views in you view controller, both with a tap gesture attached, and you want them to do something depending on which was tapped? In this case you can use the gesture's view property, with casting. The skeleton code:

    let imageView1 = UIImageView()
    let imageView2 = UIImageView()
    let tapRecognizer1 = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
    imageView1.addGestureRecognizer(tapRecognizer1)
    let tapRecognizer2 = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
    imageView2.addGestureRecognizer(tapRecognizer2)
    
    
    func imageTapped(recognizer: UITapGestureRecognizer) {
        print("Image was tapped")
        let imageView = recognizer.view as? UIImageView
        if imageView != nil {
            //do something here
        }
    }
    

    Be aware, the skeleton may have a syntax error in the casting. The logic is solid, I am writing this online, not in Xcode. Basically cast the attached UIView as an image view and check for nil.

  3. The case I usually use if if you have several subviews and you want to know which one was tapped. In your case, suppose you had two UIImageViews like above, but you have the tap recognizer attached to the main view? In this case you can use the hitTest() method. Skeleton code:

    let imageView1 = UIImageView()
    let imageView2 = UIImageView()
    let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
    imageView1.addGestureRecognizer(self.view)
    
    func imageTapped(recognizer: UITapGestureRecognizer) {
        print("Image was tapped")
        let p = recognizer.location(in: view)
        if imageView1.layer.hitTest(p) != nil {
            // do something here to imageView1
        } else if imageView2.layer.hitTest(p) != nil {
            // do something here to imageView2
        }
    }
    

    I prefer to use layers, but views have a hitTest() method also. The value to this is a view with 7 subviews can be coded with a single gesture, and if you care to ignore the tap on 3 of those subviews (and when tapped on the main view), you can.

    Regard to the last one, I've had a small example project for a while. It contains an image view with two colored labels as subviews. When either label is tapped, a counter is incremented. I've used this in 1-2 of my answers and I thought I'd put it in a GitHub repository. Feel free to look at it: Image with Labels.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
  • What if I want the function imageTapped to take an image as it's parameter because I want to do something with the image when it is tapped...I get that this question only said to print something when the image is tapped but what if i want to do something to an image. – tee May 25 '17 at 23:00
  • 1
    You can't pass a parameter like that. But here what you can do - (1) use a view/layer `hitTest()` method if you need to find if it's been tapped. I'll add a bit more code to my answer for that. (2) If you *already* know that the `UIImageView` was tapped - which you may with the code you've shown - then declare that `UIImageView` more globally and within you function do what is needed. –  May 26 '17 at 00:38
  • It took me 30 minutes to write the edit, sorry. But I have three options explained, two with skeleton code. I also added a small Swift 3 Xcode project to a GitHub repository. Hope this helps you out! –  May 26 '17 at 01:12
  • When I write this code, it adds the image to the view. Is there a way to do this with an image I already added and set up in the storyboard? – Marjorie Pickard Oct 23 '17 at 18:49
  • Sorry, this is an answer to a question from a few months ago - my turn to ask you a few questions @MarjoriePickard. :-) (And remember, you may do best to ask a new question.) (1) Is there a way to do... exactly what? Check when a user apps on an image added and set up in a Storyboard? Either of the two code snippets *should* work, just don't (a) *define* the image views in code but instead (b) create `IBOutlets` for them and then (c) in `viewDidLoad` add the tap recognizers. The *key* thing is understanding the the `let` statement is what is *creating* them and making them outlets should work. –  Oct 23 '17 at 21:51
  • @dfd, Thank you for your questions. I actually did figure out that I did not not need to define the image views in code and just provide an outlet. I should have modified the question once I solved it. Thank you! – Marjorie Pickard Oct 29 '17 at 20:29
0

Change param type as follows:

func imageViewTapped(imageView:UITapGestureRecognizer? = nil ) {
    print("Image Clicked")
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
Jitendra Tanwar
  • 249
  • 2
  • 11
  • Your signature isn't easily understood. If the function is executed, it's **because** the sender - a UITapGestureRecognizer - **isn't** nil! Yet you declare the "gesture" to be a variable called `imageView`? Again, not easily understood. –  May 25 '17 at 16:02