CATiledLayer flashes and re-draws entirely when re-drawing a single tile

I have filed a bug report for this (FB17734946), but I'm posting it here verbatim in case others have the same issue and in hopes of getting attention from an Apple engineer sooner.


When calling setNeedsDisplayInRect on a CATiledLayer - or a UIView whose backing layer is CATiledLayer - one would expect to re-draw only a region identified by the rect passed to the method. This is even written in the documentation for the class:

"Regions of the layer may be invalidated using the setNeedsDisplayInRect: method however the update will be asynchronous. While the next display update will most likely not contain the updated content, a future update will."

However, upon calling this method, CATiledLayer redraws whole contents instead of just the tile at the specified rect, and it flashes when doing so. It behaves exactly the same as if one had called setNeedsDisplay without passing any rect; all contents are cleared and re-drawn again. I'm 100% sure I've passed in the correct rect of the exact tile that I need to redraw. I have even tried passing much smaller rects, but still the same. (And yes, the rect I've passed accounts for the current level of detail.)

I have found this GitHub repo https://github.com/frankus/NetPhotoScroller, which based on discussion from here https://forums.macrumors.com/threads/catiledlayer-blanks-out-tiles-when-redrawing.1333948/ aims at solving these issues by using two private methods on CATiledLayer class:

  • (void)setNeedsDisplayInRect:(CGRect)r levelOfDetail:(int)level;
  • (BOOL)canDrawRect:(CGRect)rect levelOfDetail:(int)level;

I have explored the repo in detail, however I wasn't able to test exactly this code from the GitHub repo. I have tried using those two private methods myself (through an Objective-C class that defines the methods in the header file and then a swift class which inherits it), but I couldn't solve the issue; the flashing and the full re-draw is still there.

After doing a lot of research, the conclusion seems to be that one cannot use CATiledLayer with contents that are downloaded remotely, on demand, as tiles are being requested.

I have, however, found one interesting thing which seems to work so far: before calling setNeedsDisplayInRect (or just setNeedsDisplay, as they behave the same for CATiledLayer in my testing), cache the current layer's contents, and after calling setNeedsDisplay (or setNeedsDisplayInRect), restore the contents back to the layer. This prevents flashing and preserves any tiles that were drawn at the time of the re-draw.

let c = tiledLayer.contents tiledLayer.setNeedsDisplay(tileRect) tiledLayer.contents = c

However! Docs clearly state the warning:

Do not attempt to directly modify the contents property of a CATiledLayer object. Doing so disables the ability of a tiled layer to asynchronously provide tiled content, effectively turning the layer into a regular CALayer object.

I believe this message implies modifying the contents property with some raw content, like image data, and that it may be safe to re-apply the existing contents (which are in my testing of type CAImageProvider) -- but I can't rely on an implementation detail in my production app.

I have tested this and confirmed that the bug appears on:

  • iPhone 14 Pro, iOS 18.5
  • iPhone 13 Pro, iOS 17.5.1
  • iPhone 5s, iOS 15.8.3
  • iPad Pro 1st gen, iPadOS 18.4.1
  • a couple simulator versions

I can also confirm that the fix (to re-apply contents property) is also working properly on all these versions.

Is this expected behavior, that tiled layer redraws itself entirely instead of redrawing specific tiles? Is it safe to modify contents of a CATiledLayer by re-applying the existing contents? If not, is there an alternative to avoid flashing?

I was using CATiledLayer a few years ago. I don't recall any flashing bug, but it was a while ago.

Those two external references you mentioned are both over a decade old.

Maybe look at this project instead: https://github.com/Siclo-Mobile/SCTiledImage

It seems at one point 5 years ago I opened a DTS asking Apple for help with CATiledLayer on macOS, referencing the above project. However, I closed the request a couple of days later saying I had figured it out, whatever "it" was.

I can tell you that I'm not using CATiledLayer anymore. At some point, I moved on to using Core Image. Then I even abandoned that for the most part. Now do most work directly in Metal.

I'm sure that my work with CATiledLayer and the Core Image equivalent helped me to better understand the problem space. Development of a complex app is not necessarily linear. You may ultimately require capabilities that CATiledLayer simply can't provide. But then you might not. Maybe if you explain what your app is trying to do, at a higher level, I could be more helpful.

I checked on the status of your bug report and it is open and under investigation at this time. Previously, I have received other reports from developers about entire view contents being drawn when calling setNeedsDisplayInRect with a smaller rectangle, but none specifically linking that to CATiledLayer or a flashing phenomena. I recommend adding a small focused sample and some directions to your bug so engineering can use that to reproduce the problem you have described.

Thanks @Etresoft. I'm aware the two references I posted are over a decade old; I went even further when exploring this, since the class itself was written long time ago, and it's not a common thing to find resources for.

Interestingly though, I haven't found this SCTiledImage previously! I'll have to give it a shot.

Thanks for clarifying Core Image and Metal usage. I have used both Metal and Core Image for various things previously, but for my current use-case, CATiledLayer is indeed the perfect fit - or correct me if you think otherwise. My use-case is displaying a very large image, which I do by rendering it in tiles. The new use-case I have is that tiles should be downloaded from a server, where they are pre-generated.

@DTS Engineer thanks a lot for the info! I have created a demo project, attached it to the report I have submitted, and I've uploaded it to GitHub as well. Here it is: https://github.com/galijot/CATiledLayer-flashing-demo

Note: flashing that I'm describing is the result of CATiledLayer (from my assumption) discarding its current contents and asynchronously re-drawing the whole view. When reloading many tiles (one after each is downloaded), this results in flashing.

Oh and btw: in my original post I accidentally wrote "iPhone 5s" instead of "iPhone 6s" (not that it probably matters much, but still).

The rest of this message will be verbatim of some more info I have posted on the report.


The project has two print statements which are crucial for debugging this:

  • "draw at position" which is printed in draw(_:) method of TiledView, which prints the position (X,Y and LOD) that it is about to draw, as well as the rect in which it draws.
  • "reload at position" which is printed in reloadTile(at:), which prints the position that it is about to re-draw, as well as the calculated rect for the tile.

You can search for "FIXME" in the project, which will lead you to the code that displays 4 possible options for testing this:

  1. re-drawing with setNeedsDisplayInRect
  2. re-drawing with setNeedsDisplay
  3. caching layer's contents and applying them after setNeedsDisplayInRect
  4. caching layer's contents and applying them after setNeedsDisplay
CATiledLayer flashes and re-draws entirely when re-drawing a single tile
 
 
Q