SwiftUI- Make Only the Visible Part of png Image Respond to Tap Gesture

157 views Asked by At

I'm working on a iOS app (using SwiftUI) in which the user must tap an image to get a response. The image overlays another background image, so it intentionally has some transparency. In some of the images there is a lot of transparent space. This allows too much tappable area in "empty space". I would like to refine it so that only the visible part is responsive to the tap gesture.

As of right now, anywhere a user taps the gesture responds...even if the user taps in a transparent part of the image. I found a similar question here Make only visible parts of png clickable UIbutton swift, but it seems to deal with multiple images in a ZStack. Here is some code that I have:

Image("dog") 
    .resizable() 
    .scaledToFit() 
    .zIndex(1) 
    .clipped() 
    .gesture(simpleTap) 
var simpleTap: some Gesture { 
   TapGesture() 
      .onEnded { _ in 
         playSound("dog.mp3") 
      } 
   } 
1

There are 1 answers

0
Sweeper On

The key here is to set the contentShape of the image to a Shape that represents the outline of the image.

If your image is a static image, then you can just use an image editor to find the outline and export it to a vector graphics format. For example, in GIMP, you can select the non-transparent part of the image, convert selection to path, then export that path to SVG. (See What's the easiest way to turn the non-transparent parts of any image file into a single svg path?) Then you can read the SVG with something like PocketSVG, get a CGPath, and create a SwiftUI Path from it.

let svgURL = Bundle.main.url(forResource: "foo", withExtension: "svg")!
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
// you can save the 2 things above somewhere else, so that it doesn't get
// recomputed every time the view updates.
Image("dog")
    .contentShape(Path(paths.first!.cgPath))
    .onTapGesture {
        print("Tapped")
    }
    .overlay { // overlay showing you where the tappable area is
        Path(paths.first!.cgPath).stroke(lineWidth: 1)
    }

If the image is resizable, you should resize the path accordingly. You can create your own Shape to do this:

struct ResizablePath: Shape {
    let path: Path
    let originalSize: CGSize
    func path(in rect: CGRect) -> Path {
        let xScale = rect.width / originalSize.width
        let yScale = rect.height / originalSize.height
        print(path.boundingRect)
        return path.applying(.init(scaleX: xScale, y: yScale).translatedBy(x: rect.minX, y: rect.minY))
    }
}
let svgURL = Bundle.main.url(forResource: "foo", withExtension: "svg")!
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
// get the original size from a UIImage
let size = UIImage(named: "dog")!.size
Image("dog")
    .resizable()
    .contentShape(ResizablePath(path: Path(paths.first!.cgPath), originalSize: size))
    .scaledToFit()
    .onTapGesture {
        print("Tapped")
    }
    .overlay {
        ResizablePath(path: Path(paths.first!.cgPath), originalSize: size).stroke(lineWidth: 1)
    }