0

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!

Pierce
  • 3,148
  • 16
  • 38

1 Answers1

3

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.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thank you for your quick response! I tried implementing what you recommended and it definitely helped! It didn't totally solve the problem, as I still do have some choppy animation but it reduced the issue to about 50% of what it was before, so I thank you. I'm going to continue to do some profiling and see if I can totally isolate the problem and fix it, if not I may have to re-evaluate my design. If I don't receive any other advice that helps I'll accept this as the right answer. – Pierce Apr 02 '16 at 22:10
  • @Pierce glad to hear it helped! One thing that could maybe be a problem is some other answers have pointed out that setting `shouldRasterize` to `true` on multiple sublayers can impact performance - you should only set it to `true` on a single superlayer. – Hamish Apr 02 '16 at 22:14
  • That's not a bad idea. In fact, I forgot you could do that! I'd like to explore other avenues first though, as that would require me to have to reconstruct a lot of code. Thank you for your help. I will continue to post any updates. – Pierce Apr 02 '16 at 23:01
  • I'm starting to think that may be the best solution though. Because you are right my view doesn't need to be changed after it's created. I may just have to suck it up and go that route. – Pierce Apr 02 '16 at 23:03
  • @Pierce Actually thinking about it - my suggestion last night of rendering the view as an image wasn't a great idea, as it would just create a memory problem. Setting `shouldRasterize` to `true` renders the layer as a bitmap before compositing, so you should be getting the performance boost anyway. How many views are you displaying on screen at any one given time? Larger views on the screen, so that you are displaying less of them is usually much better for performance. Would it also be possible for you to merge your views into a single view, with a single path made up of all your subpaths? – Hamish Apr 03 '16 at 12:58
  • ~ Yeah I actually sat and thought about it, and I can't render the views as `UIImage` objects anyways because periodically the user may have to go back and edit, so I have to keep it how it originally was. And I have noticed that when I use larger views (so there are less displayed on the screen at once) the performance increases considerably, but I need the user to choose that as an option. I've also noticed that when the views are spaced out decently, there is no performance drag...but when parts of them are overlapping that's when I get serious lag with animations. – Pierce Apr 03 '16 at 17:00
  • ~ I should clarify that a little bit...the way my app is set up, the views would never appear to overlap but sometimes I have the user move two together to try and connect their different geometrical idiosyncrasies (Imagine like making custom tetris blocks and then matching them later). Anyways they may not appear to overlap, but the parts of the view that are being masked are actually overlapping, I assume however that those have an alpha mask, making them appear to be translucent. When multiple views have overlapping masked areas thats when performance issues really show. – Pierce Apr 03 '16 at 17:07
  • 1
    I got it working great! And all I did differently was I re-read your answer...originally I had missed a very crucial piece of advice. Where you said to create a whole new `CAShapeLayer` to draw the path to go along with the one that I was using as a mask! I can't believe I missed that! I've spent days frustrated over this. Originally when I got rid of the `UIBezierPath` and converted it to a `CAShapeLayer`, I was using that layer to stroke the path AND as the mask. By creating two `CAShapeLayer` objects, one for the stroke and one for the mask, my animations are now flawless! THANK YOU! – Pierce Apr 03 '16 at 20:54
  • @Pierce happy to help! :) – Hamish Apr 03 '16 at 20:55
  • ~ I would also like to add that another thing that was very important was removing any `shouldRasterize` settings on a sublayer and keeping it only on the superlayer, that was also crucial! Can't thank you enough, if I would have actually payed attention while reading the whole thing I could have saved myself around 24 hours of frustration. :) – Pierce Apr 03 '16 at 20:58