2

I have a subclass of UIView containing a bunch of graphics drawn using CGPAths. I need to know when a touch hits one of these paths, and which one.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches {
        for p in myDrawnPaths {
           if(p.contains(t.location(in: self)) {
              doStuffWith(p)
           }
        }
    }
}

Sometimes this code results in doStuffWith() carried out on more than one path, including those far away from the hit location. I did some inspection and found out something really odd about the paths that shouldn't be affected:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches {
        for p in myDrawnPaths {
           print(p.contains(t.location(in: self))) //true
           print(p.boundingBox.contains(t.location(in: self))) //false!
        }
    }
}

Huh? Shouldn't the bounding box contain the entire path, meaning that a point inside the path is guaranteed to be inside the bounding box?

The problem only arises in such tests -- when I use CoreGraphics API to draw and animate with the same CGPaths, all the displays are correct.

UPDATE

I've tried to reproduce the issue with simple experimental paths in Playgrounds, but was unable to, so I had to take this from my actual app.

(75.0, 264.0) is in 
Path 0x600003643960:
  moveto (252.02, 287.067)
    lineto (259.489, 286.324)
    lineto (259.567, 286.754)
    lineto (263.438, 286.167)
    lineto (268.131, 285.503)
    lineto (268.17, 285.268)
    lineto (268.248, 284.681)
    lineto (269.069, 283.235)
    lineto (269.851, 282.57)
    lineto (269.773, 280.576)
    lineto (270.399, 279.95)
    lineto (270.829, 279.833)
    lineto (270.907, 278.425)
    lineto (271.494, 277.252)
    lineto (271.924, 277.486)
    lineto (272.002, 277.721)
    lineto (272.315, 277.799)
    lineto (273.058, 277.408)
    lineto (272.901, 273.85)
    lineto (271.65, 270.643)
    lineto (270.751, 267.085)
    lineto (269.812, 265.834)
    lineto (268.796, 265.13)
    lineto (268.17, 265.56)
    lineto (266.645, 266.264)
    lineto (265.902, 268.219)
    lineto (264.846, 269.666)
    lineto (264.416, 269.9)
    lineto (263.83, 269.666)
    curveto (263.83, 269.666) (262.813, 269.079) (262.891, 268.845)
    curveto (262.969, 268.61) (263.087, 266.889) (263.087, 266.889)
    lineto (264.416, 266.381)
    lineto (264.729, 265.052)
    lineto (264.964, 264.035)
    lineto (265.902, 263.409)
    lineto (265.785, 259.499)
    lineto (265.159, 258.6)
    lineto (264.651, 258.287)
    lineto (264.338, 257.466)
    lineto (264.651, 257.153)
    lineto (265.276, 257.27)
    lineto (265.355, 256.644)
    lineto (264.338, 255.784)
    lineto (263.83, 254.767)
    lineto (262.813, 254.767)
    lineto (261.053, 254.181)
    lineto (258.903, 252.851)
    lineto (257.847, 252.851)
    lineto (257.612, 253.086)
    lineto (257.221, 252.891)
    lineto (256.009, 251.991)
    lineto (254.875, 252.695)
    lineto (253.741, 253.594)
    lineto (253.858, 255.002)
    lineto (254.249, 255.119)
    lineto (255.07, 255.315)
    lineto (255.266, 255.628)
    lineto (254.249, 255.941)
    lineto (253.233, 256.058)
    lineto (252.646, 256.762)
    lineto (252.529, 257.583)
    lineto (252.646, 258.209)
    lineto (252.763, 260.359)
    lineto (251.356, 261.18)
    lineto (251.121, 261.102)
    lineto (251.121, 259.46)
    lineto (251.629, 258.521)
    lineto (251.864, 257.583)
    lineto (251.551, 257.27)
    lineto (250.808, 257.583)
    lineto (250.417, 259.225)
    lineto (249.361, 259.655)
    lineto (248.657, 260.398)
    lineto (248.579, 260.789)
    lineto (248.814, 261.102)
    lineto (248.579, 262.119)
    lineto (247.68, 262.314)
    lineto (247.68, 262.745)
    lineto (247.993, 263.683)
    lineto (247.563, 266.068)
    lineto (246.937, 267.632)
    lineto (247.172, 269.47)
    lineto (247.367, 269.9)
    lineto (247.054, 270.839)
    lineto (246.937, 271.152)
    lineto (246.82, 272.208)
    lineto (248.227, 274.554)
    lineto (249.361, 277.095)
    lineto (249.948, 278.972)
    lineto (249.635, 280.81)
    lineto (249.244, 283.156)
    lineto (248.306, 285.19)
    lineto (248.188, 286.246)
    lineto (246.937, 287.458)
    closepath
  moveto (233.916, 259.147)
  moveto (233.407, 258.717)
  moveto (232.703, 254.65)
  moveto (231.257, 254.142)
  moveto (230.592, 253.242)
  moveto (225.665, 252.148)
  moveto (224.57, 251.717)
  moveto (221.403, 250.857)
  moveto (218.352, 250.466)
  moveto (216.827, 248.394)
  moveto (217.101, 248.198)
  moveto (218.157, 247.885)
  moveto (219.565, 246.986)
    lineto (219.565, 246.595)
    lineto (219.799, 246.36)
    lineto (222.145, 245.969)
    lineto (223.084, 245.226)
    lineto (224.804, 244.405)
    lineto (224.883, 243.897)
    lineto (225.626, 242.763)
    lineto (226.33, 242.45)
    lineto (226.838, 241.746)
    lineto (227.737, 240.847)
    lineto (229.458, 239.908)
    lineto (231.296, 239.713)
    lineto (231.726, 240.143)
    lineto (231.608, 240.534)
    lineto (230.162, 240.925)
    lineto (229.575, 242.137)
    lineto (228.676, 242.45)
    lineto (228.48, 243.388)
    lineto (227.542, 244.64)
    lineto (227.424, 245.656)
    lineto (227.737, 245.852)
    lineto (228.128, 245.422)
    lineto (229.536, 244.288)
    lineto (230.044, 244.796)
    lineto (230.944, 244.796)
    lineto (232.195, 245.187)
    lineto (232.782, 245.617)
    lineto (233.368, 246.83)
    lineto (234.424, 247.885)
    lineto (235.949, 247.807)
    lineto (236.535, 247.416)
    lineto (237.161, 247.924)
    lineto (237.787, 248.12)
    lineto (238.295, 247.807)
    lineto (238.725, 247.807)
    lineto (239.351, 247.416)
    lineto (240.915, 246.008)
    lineto (242.245, 245.578)
    lineto (244.825, 245.461)
    lineto (246.585, 244.718)
    lineto (247.602, 244.21)
    lineto (248.188, 244.288)
    lineto (248.188, 246.517)
    lineto (248.384, 246.634)
    lineto (249.518, 246.947)
    lineto (250.261, 246.751)
    lineto (252.646, 246.126)
    lineto (253.076, 245.696)
    lineto (253.663, 245.891)
    lineto (253.663, 248.628)
    lineto (254.914, 249.84)
    lineto (255.422, 250.075)
    lineto (255.931, 250.466)
    lineto (255.422, 250.583)
    lineto (255.109, 250.466)
    lineto (253.663, 250.271)
    lineto (252.842, 250.505)
    lineto (251.942, 250.427)
    lineto (250.691, 251.014)
    lineto (249.987, 251.014)
    lineto (247.719, 250.505)
    lineto (245.686, 250.583)
    lineto (244.943, 251.6)
    lineto (242.205, 251.835)
    lineto (241.267, 252.148)
    lineto (240.837, 253.36)
    lineto (240.328, 253.79)
    lineto (240.133, 253.712)
    lineto (239.546, 253.086)
    lineto (237.787, 254.025)
    lineto (237.552, 254.025)
    lineto (237.122, 253.399)
    lineto (236.809, 253.477)
    lineto (236.066, 255.198)
    lineto (235.675, 256.762)
    lineto (234.424, 259.46)
    closepath
  moveto (222.849, 237.367)
  moveto (223.553, 236.545)
  moveto (224.413, 236.233)
  moveto (226.525, 234.708)
  moveto (227.424, 234.473)
  moveto (227.62, 234.668)
  moveto (225.626, 236.663)
  moveto (224.335, 237.406)
  moveto (223.514, 237.758)
    closepath
  moveto (257.221, 250.31)
  moveto (257.456, 251.287)
  moveto (258.707, 251.365)
  moveto (259.215, 250.896)
    curveto (259.215, 250.896) (259.176, 250.31) (259.059, 250.271)
    curveto (258.942, 250.192) (258.433, 249.528) (258.433, 249.528)
    lineto (257.573, 249.606)
    lineto (256.947, 249.684)
    lineto (256.83, 250.114)

As we see, all the points in the path have the X-coordinate in the 200s, but a point with X-coordinate of 75 is computed to be within that path.

The issue goes away if I tack on a close command to the path, but that brings up additional questions:

  1. How does CGPath's contains work on open paths, and where, if anywhere, is it documented?

  2. Why does this affect the results of contains method and not anything I see on screen? For instance, if I tell the corresponding CALayer to be filled with a certain color, the color doesn't spill out beyond where it should.

executor21
  • 4,532
  • 6
  • 28
  • 38

1 Answers1

4

After experimenting with this in a playground I discovered that the CGRect.contains(:) method returns false for points that have an x value that is greater than or equal to the maxX value or a y value that is greater than or equal to the maxY value.

It turns out that the documentation for CGRect says something similar:

A point is considered inside the rectangle if its coordinates lie inside the rectangle or on the minimum X or minimum Y edge.

I would argue that the documentation is poorly worded because the intersection of the max X and min Y edges are not included same for the intersection the min X and max Y edges.

Here is code to demonstrate this:

let rect = CGRect(x: 0, y: 0, width: 200, height: 200)
rect.contains(CGPoint(x: 0, y: 0))                 // true
rect.contains(CGPoint(x: rect.midX, y: 0))         // true
rect.contains(CGPoint(x: 0, y: rect.midY))         // true
rect.contains(CGPoint(x: 200, y: 200))             // false
rect.contains(CGPoint(x: 200, y: 0))               // false
rect.contains(CGPoint(x: 0, y: 200))               // false
rect.contains(CGPoint(x: 0, y: rect.maxY))         // false
rect.contains(CGPoint(x: rect.maxX, y: 0))         // false
rect.contains(CGPoint(x: rect.maxX, y: rect.maxY)) // false
rect.maxX                                          // 200
rect.maxY                                          // 200

Also, the documentation states that the boundingBox contains all points for the path including control points for Bézier and quadratic curves. You may want to use boundingBoxOfPath instead.

Joe VB
  • 51
  • 3