Search code examples
iosswiftvectordrawinguibezierpath

How to subtract the intersection of UIBezierPath A&B from A?


Suppose we have two UIBezierPaths, path1 and path2... (that have already been defined relative to view bounds at runtime and already exist as properties of the same view).

We want to get a new path of type UIBezierPath, path3, that is the result of subtracting path2 from path1:

enter image description here

The way this is done (as seen here) is to do this:

path1.append(path2.reversing())

BUT, that only seems to work for circumstances where path1 fully encompasses path2.

For example, consider the case where there is only a partial intersection -- path1 does not fully encompass path2. This is what happens if we apply the same method as above:

enter image description here

In Android, the answer is:

path1.op(path2, Path.Op.DIFFERENCE);

So... is there an equivalent simple operation in IOS?

If not, is there a function that could be written as:

func returnPath2CutOutOfPath1(path1: UIBezierPath, path2: UiBezierPath) -> UIBezierPath {

// the mystery lies within these here parts. :)

}

Solution

  • There is no direct way to get the differences of UIBezierPaths as a new path on iOS.

    Testcase

    private func path2() -> UIBezierPath {
        return UIBezierPath(rect: CGRect(x: 100, y: 50, width: 200, height: 200))
    }
    
    private func path1() -> UIBezierPath {
        return UIBezierPath(rect: CGRect(x: 50, y: 100, width: 200, height: 200))
    }
    

    Starting point: to simply show which path represents what: path 1 is yellow and path 2 is green:

    starting point

    Possiblity 1

    You have probably already seen this possibility: In the link you mentioned in your question, there is also a clever solution if you only have to perform a fill operation.

    The code was taken from this answer (from the link you posted) https://stackoverflow.com/a/8860341 - only converted to Swift:

    func fillDifference(path2: UIBezierPath, path1: UIBezierPath) {
        let clipPath = UIBezierPath.init(rect: .infinite)
        clipPath.append(path2)
        clipPath.usesEvenOddFillRule = true
    
        UIGraphicsGetCurrentContext()?.saveGState()
        clipPath.addClip()
        path1.fill()
        UIGraphicsGetCurrentContext()?.restoreGState()
    }
    

    So this fills a path, but does not return a UIBezierPath, which means that you cannot apply outlines, backgrounds, contour widths, etc., because the result is not a UIBezierPath.

    It looks like this:

    fill only

    Possiblity 2

    You can use a third-party library, e.g. one from an author named Adam Wulf here: https://github.com/adamwulf/ClippingBezier.

    The library is written in Objective-C, but can be called from Swift.

    In Swift, it would look like this:

    override func draw(_ rect: CGRect) {
        let result = self.path1().difference(with: self.path2())
    
        for p in result ?? [] {
            p.stroke()
        }
    }
    

    If you want to use this library, you have to note a small hint: in Other Linker Flags in the project settings, as described by the readme, "-ObjC++ -lstdc++" must be added, otherwise it would be built without complaints, but would silently not load the frameworks and eventually crash, since the UIBEzierPath categories are not found.

    The result looks like this:

    real bezier path result

    So this would actually give your desired result, but you would have to use a 3rd party library.