11

I'm trying to recreate something for a iOS app (Swift) which I already made in HTML5 (using map area-coords).

I want to show a human body diagram that interacts with user clicks/touches. The body exists of let's say 20 different parts, and the user can select one or more body-parts. For every body-part there is a selected-state image that should appear when a part is selected. Clicking on a selected part will deselect it. After selecting one or more parts, the user can continue and will get some information about these parts in a next viewcontroller. I am attaching a simplified image to explain my goal.

bodyparts selection

Can somebody explain what the best way is to achieve this goal? Is there a comparable technique that can be used in Swift to create such an interactive image?

Thanks!

rptwsthi
  • 10,094
  • 10
  • 68
  • 109
klaaskox
  • 329
  • 4
  • 15
  • So you want it to bee 2 D or 3D? – rptwsthi Mar 19 '16 at 13:14
  • I'd recommend **hitTest:** and CALayer, here's the [equivalent in ObjC](http://sketchytech.blogspot.co.uk/2012/10/the-secret-life-of-calayer-part-2.html) which I can happily translate to Swift if it looks useful – sketchyTech Mar 19 '16 at 13:17
  • @sketchyTech `CALayer` `hitTest` only works with the rectangular bounds of a layer. The OP's illustration shows irregular shapes. I think using bezier paths and `containsPoint` would be better for this case. (Plus layers are a lower-level system component that introduce another level of complexity.) They are certainly powerful, but I don't think they're worth that additional complexity in this case. – Duncan C Mar 19 '16 at 13:27
  • yes, you're right you'd need to do something like combine CAShapeLayer with (containsPoint)[http://stackoverflow.com/questions/29767663/creating-an-irregular-uibutton-in-swift-where-transparent-parts-are-not-tappable] to get the precision – sketchyTech Mar 19 '16 at 14:04
  • Thanks! I indeed need irregular shapes, so I guess I will dive into the CAShapeLayer. From what I read I have to create a CAShapeLayer for each bodypart. To define the shape of each bodypart I can define a UIBezierPath as path. Should I position all these CAShapeLayers over a UIImageView with the basic-unselected body-image? How can I do that? Normally I would use constraints to position a UIView/Button etc., but I expect this can't be done with a CAShapeLayer? – klaaskox Mar 19 '16 at 16:29
  • 1
    There are no Auto Layout constraints for layers in iOS what you'd need to do is have, for example, the UIImageView constrained and then add the layers to this as sublayers. Making sure to position each part relative to body image in terms of position and size. Rotation shouldn't be a problem because the image view is constrained and the layers stay fixed within it but if size changes as in split-screen then the layers will need to be resized and positions but keeping everything relative this shouldn't be an issue. Your other choice is to use a series UIView subclasses and the **drawRect:** – sketchyTech Mar 20 '16 at 11:47
  • I got the shape-positioning working, but I'm still trying to figure out how the I shoulf bind the touch-events. Tried to use touchesBegan, and loop all sublayers to perform a hittest. Is this the correct way? – klaaskox Mar 22 '16 at 22:04
  • UPDATE: got it working with: if CGPathContainsPoint(layer.path, nil, position, false) { ... } – klaaskox Mar 22 '16 at 22:56
  • Great stuff, remember you can post your own answer to this question. – sketchyTech Mar 23 '16 at 07:03
  • Done! Now I'm trying to apply a gradient to the shapes; I want the color of the highlighted shapes to fade out on the edges. Should I use a CAShapeGradient for each part and add the CAShapeLayers I already created as masks? – klaaskox Mar 23 '16 at 08:45

3 Answers3

0

There is no built-in mechanism for detecting taps in irregular shapes. The standard UIView tap detection uses frame rectangles. (Likewise with CALayers, as suggested by sketchyTech in his comment above.)

I would suggest drawing your body regions as bezier paths. You could create a custom subclass of UIView (BodyView) that would manage an array of BodyRegion objects each of which include a bezier path.

The UIBezierPath class includes a method containsPoint that lets you tell if a point is inside the path. Your BodyView could us that to decide which path object was tapped.

Bezier paths would handle both drawing your body regions and figuring out which part was tapped.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • did you have any sample on this,I'm looking for something similar too – srivas Sep 11 '17 at 06:55
  • @srivas, no, I don't have an example. It's not that complicated. Give it a try using the `containsPoint()` method I describe, and post a question if you can't get it working correctly. – Duncan C Sep 11 '17 at 14:59
0

A very simple way to achieve this, would be to place invisible buttons over the areas, then hook every one up to an IBAction and put anything you want to happen inside.

@IBAction func shinTapped(sender: UIButton) {
    tappedCounter = 0
    if tappedCounter == 0 {
      // set property to keep track
      shinSelected = true
      // TODO: set button image to selected state
      // increment counter
      tappedCounter++
      }else{
      // set property to keep track
      shinSelected = false
      // TODO: set button image to nil
      // reset counter
      tappedCounter = 0
  }

This might get a little tricky to layout, so that every button sits in the right spot, but if you work with size classes it is totally doable. I am sure there is a more elegant way to do it, but this is one way.

Myriam
  • 73
  • 8
0

Fixed!

First I added sublayers to the UIImageView like this

var path = UIBezierPath()
path.moveToPoint(CGPointMake(20, 30))
path.addLineToPoint(CGPointMake(40, 30))

// add as many coordinates you need...

path.closePath()

var layer = CAShapeLayer()
layer.path = path.CGPath
layer.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.5).CGColor
layer.hidden = true

bodyImage.layer.addSublayer(layer)

Than I overrided the touchesbegan function in order to show and hide when the shapes are tapped.

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

    if let touch = touches.first! as? UITouch {
        // get tapped position
        let position = touch.locationInView(self.bodyImage)

        // loop all sublayers
        for layer in self.bodyImage.layer.sublayers! as! [CAShapeLayer] {

            // check if tapped position is inside shape-path
            if CGPathContainsPoint(layer.path, nil, position, false) {
                if (layer.hidden) {
                    layer.hidden = false
                }
                else {
                    layer.hidden = true
                }
            }
        }
    }
}
klaaskox
  • 329
  • 4
  • 15
  • Hi, I'm trying to do something very similar, but I'm sort of new to this. Could you possibly post a sample code on github for me to get started on? I'm not understanding some of this code.Thanks! – Pangu Apr 27 '16 at 15:41
  • @Pangu did you any sample on this,I'm looking for something similar too – srivas Sep 11 '17 at 06:54
  • I am just wondering how you handle different devices and the scaling that's applied to the imageView. This will throw off the Shape Layer's coordinates. – Moebius Nov 05 '18 at 19:34