Search code examples
swiftuiscrollviewcalayeranimatewithdurationrasterizing

Animation and Scrolling Performance Seriously Lagging Using CAShapeLayers for Masking


I'm asking this question as a last resort because any solution I've found and tried has yet to produce a positive result. I am designing an app where a user can manipulate a UIView and make a custom shape that is defined by a UIBezierPath. Without getting into too many specifics, and to avoid boring you this is basically what's going on:

override func drawRect(rect: CGRect) {

    // Create the path
    var path = UIBezierPath()

    // ...... The path is defined, set to be drawn in the view's frame

    // Draw the stroke of the actual path, close it and clip.
    path.lineWidth = 2.0
    UIColor.whiteColor().setStroke()
    path.stroke()
    path.closePath()
    path.addClip()

    // Create a CAShapeLayer to crop everything outside the path
    let maskForPath = CAShapeLayer()
    maskForPath.path = path.CGPath
    self.layer.mask = maskForPath
}

So that works great, and as the user continues to use the app, they create more and more shapes and their previously created shapes get stored in a UIScrollView for them to see, select and edit later on. Anyways once multiple shapes have been created (say 15 or more), any animations that I perform using UIView.animateWithDuration get EXTREMELY choppy, and the same goes for the scrolling inside the UIScrollView. It's frustrating because everything works great and is fluid up until a large amount of views are created. Also, I've tried manually moving the views using touchesBegan and touchesMoved and when I move around the views that way they move just fine, there is no lag or anything.

I've exhausted every solution I could find thus far. Here are some of the things I've tried to this point:

1) a) layer.shouldRasterize = true

b) layer.rasterizationScale = UIScreen.mainScreen().scale

2) layer.drawsAsynchronously = true

3) AsyncDisplayKit

4) layer.allowsEdgeAntialiasing = true

None of these solutions has yielded a positive result. Can anyone think of anything I may be missing, or have an idea that could solve this issue? It doesn't seem to be a memory problem, as even when I have a lot of these views nested inside a UIScrollView, my app is only using an average of 30mb of memory. Also, the impact of the CPU seems to be minimal. I'm not using any shadows, and I've tried testing this on an iPhone 5s, 6, and 6s Plus all with the same result, so it doesn't appear to be an issue with the device. Hopefully someone can help me find a solution because it makes the app look like garbage and the system isn't even being taxed as far as I can tell.

I appreciate any help! Thanks in advance!


Solution

  • drawRect can sometimes be a drag on performance.

    I would therefore suggest using a CAShapeLayer instead to stroke your path.

    You'll probably want to create a new shape layer for this, as you're already using your current one for masking. You'll want to set the strokeColor and lineWidth on it - as well as setting the fillColor to clearColor if you just want the stroke (and no fill).

    You can then remove your drawing code - and therefore remove the drawRect altogether (which should be an instant boost to performance, as the UIView has to do extra processing for custom drawing). You should also find that all the above things you've tried to do to optimise it should work way better.

    Unlike Core Graphics drawing, Core Animation is able to leverage the GPU - so usually fairs much better in terms of performance.

    You could also boost your performance significantly by eliminating the layer masking to begin with. If your view simply has a solid background color - you could simply set the fillColor on your stroke shape layer to achieve this instead, and ditch the masking.

    Also, as some of the related answers have said, there can sometimes be a problem when setting shouldRasterize to true on multiple sublayers. You should only be setting it to true on a single superlayer.

    Another potential idea to improve performance is to merge all your views into a single view - and merge your paths together into a single path made up of subpaths. You could then display this path with a masking and stroking layer.