Tom Lokhorst Tom Lokhorst - 1 month ago 20
iOS Question

CATiledLayer shows previous tiles

When invalidating a view backed by a CATiledLayer, a previous tile remains "stuck" and isn't correctly invalidated.

This seems to happen when the view is invalidated (on the main thread), while at the same time, the tile render threads are still working on a previous version of the tile. Instead of caching the new version of the tile, the previous version is cached.

The view backed by CATiledLayer is a subview of a UIScrollView and is zoomable. The rendering of a tile can be expensive and can use the render thread for 10ms.

Example



Example code that demonstrates this issue: https://github.com/Q42/CATiledLayerBug


  1. In a CATiledLayer, start rendering all red tiles (this would take about 3 seconds to complete)

  2. Each render step takes about 10ms

  3. During rendering (after 800ms), invalidate the complete view:
    tiledView.setNeedsDisplay()

  4. Start rendering all gray tiles (this again takes about 3 seconds)

  5. Two tiles (randomly?) remain red, instead of becoming gray.



See the
update
function here: https://github.com/Q42/CATiledLayerBug/blob/master/TiledLayerTest/ViewController.swift#L45

screenshot

Workaround?



This seems to be a bug in the implementation of
CATiledLayer
. Since I can't fix that, does anyone know of a good workaround for this issue?

I've filed a radar for this: http://www.openradar.me/28648050

Answer

Based on some further logging I've added to the example project, I think the problem is this:

The CATiledLayer has two render threads that draw each tile. If, during the execution of a draw(_: CGRect) call a setNeedsDisplay is called, the current execution of the draw call is finished and the result is cached. The cached value is based on the previous "data source" (just the tile color in this example), rather that the updated data source.

An Apple support engineer provided me with a workaround:

  • Add an updateID field to the TiledView
  • Add the start of a draw(_: CGRect) call save the current updateID
  • When the "data source" changes, change the updateID
  • Add the end of the draw(_: CGRect) call, compare the saved updateID with the current one.
  • If the ids differ, schedule a new setNeedsDisplay call.

Extract:

override func draw(_ rect: CGRect) {
  let originalID = updateID

  // all actual (slow) drawing code here...

  if originalID != updateID {

    // dispatch a redraw request, but wait a little while first
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(17)) {
      self.layer.setNeedsDisplayIn(rect)
    }
  }
}
Comments