Create clickable body diagram with Swift (iOS)
Asked Answered
S

3

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!

Solley answered 19/3, 2016 at 13:5 Comment(10)
So you want it to bee 2 D or 3D?Boxboard
I'd recommend hitTest: and CALayer, here's the equivalent in ObjC which I can happily translate to Swift if it looks usefulCalamander
@Calamander 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.Catalogue
yes, you're right you'd need to do something like combine CAShapeLayer with (containsPoint)[#29768163 to get the precisionCalamander
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?Solley
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:Calamander
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?Solley
UPDATE: got it working with: if CGPathContainsPoint(layer.path, nil, position, false) { ... }Solley
Great stuff, remember you can post your own answer to this question.Calamander
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?Solley
S
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
                }
            }
        }
    }
}
Solley answered 23/3, 2016 at 8:19 Comment(3)
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!Elisaelisabet
@Elisaelisabet did you any sample on this,I'm looking for something similar tooClarino
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.Tentacle
C
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.

Catalogue answered 19/3, 2016 at 13:22 Comment(2)
did you have any sample on this,I'm looking for something similar tooClarino
@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.Catalogue
W
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.

Woolley answered 19/3, 2016 at 13:46 Comment(0)
S
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
                }
            }
        }
    }
}
Solley answered 23/3, 2016 at 8:19 Comment(3)
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!Elisaelisabet
@Elisaelisabet did you any sample on this,I'm looking for something similar tooClarino
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.Tentacle

© 2022 - 2024 — McMap. All rights reserved.