This week I'd like to talk about an issue I ran into with the current app I am working on. For A/B testing reasons I added a collectionview to show the dataset to the user in a different view layout. After I implemented the feature, I noticed that there was something weird going on while I was scrolling. Cells seemed to wobble a bit and the scrolling itself had a slight jerkiness to it. So I started thinking and came up with my usual suspects:
- opacity issues / blended layers?
- offscreen rendering?
- too much work on the main thread?
- blocking calls on the main thread?
I started up instrument's Core Animation tool, hoping it would shed some light on my current problem. I switched on color blended layers and as expected nearly all of the collectionview cell showed up in green. I moved on and enabled offscreen rendering and again, everything seemed normal. There didn't even seem to be a problem with my scrolling performance since the frame rate peaked constantly between 57 fps and 60 fps. So I quit the Core Animation tool and selected Time Profiler instead, hoping again it would tell me what was going on.
I started Time Profiler in my default configuration i.e. 'separate by thread' and 'hide system libraries' selected. Former to enable me to see what's happening on the main thread and latter to reduce noise by showing me data for my code only. Time Profiler told me several things.
First of all, it told me that Optimizely, the A/B framework I was using, had a UIImage category which overrode the default implementation for imageNamed:. I used this method to show a placeholder image until I retrieved the image from the API. Optmizely's category defaulted to pathForResource:ofType: which in contrast to imageNamed: doesn't offer any caching.
Second of all, it told me that there is nothing wrong with my main thread. Neither were there any blocking calls nor did I do too much work on it. In fact. cellForItemAtIndexPath:'s contribution including image assignment (retrieval happened of course on the background thread) to the overall CPU utilization was neglectable. So none of my usual suspects I listed above nor Optimizely was responsible for this scrolling behavior. That's when I decided to go back to Xcode to play around with the implementation.
I removed all image and label initialization code and started the app again. Now there was no problem at all, scrolling was super smooth. So I started adding things back in. First I added back in the image initialization and retrieval code. Rerunning the app showed me that everything was still fine. So I added the label initialization code again. With the first label initialization back in, my weird scrolling behavior returned.
I usually setup my cells in a storyboard and I am a big fan of auto layout. When working with auto layout I often use helper views to, well, help me laying out my views. Since the aforementioned label initialization didn't really do much beyond setting a label's text property, I thought that it must have something to do with the layout engine and that a good start would be to ease its work by simplifying my view hierarchy. That's what I did, but it didn't actually solve my problem.
Since my collectionview was using a UICollectionViewFlowLayout, I checked the class for some properties that might be useful. When I couldn't find anything, I moved on to check the header files of UICollectionView and then UICollectionViewCell. And that's when I found preferredLayoutAttributesFittingAttributes:. Hoping it would ease layout calculation, I overrode it in my cell, provided the required frame sizes and reran the app. Lo and behold, scrolling was super smooth. I added back the whole cell initialization I commented out before and gave it another try. The performance was still excellent.
To reconfirm the behavior I started Time Profiler again. This time however, I didn't hide CPU utilization of system libraries. Now I saw that a big chunk of the overall CPU time was indeed spent in the layout stage.
I guess most of the time there is not just one path to success. Time profiler could have told me what was going on early on, but this time it was a good hypothesis and some Xcode debugging that did the trick.