Luke Zhao iOS Developer ★ UI & 3D Design ★ Tokyo, Japan.

.useGlobalCoordinateSpace explained

I got asked a few time on an issue that developers face when using Hero. The animation that Hero produces is almost correct. But there are some views which are suppose to be on top, get obscured during the transition and suddently appear after the transition finishes.

The exact detail can be found in this issue: https://github.com/lkzhao/Hero/issues/413 And the solution I posted there is to use the .useGlobalCoordinateSpace modifier.

Eventhough there is a simple solution, the reason why it works is not exactly clear to most devs. So in this post I want to clearify what .useGlobalCoordinateSpace modifier does and to give you guys some hint on how Hero construct the view hierarchy used during the transition.

Examples

Let me give you some examples to demonstrate a few situations.

Suppose you have this structure:

RootViewA
|- imageViewA (id: image)

RootViewB
|- imageViewB (id: image)
|- buttonB

During the transition. Hero will produce the following:

Container View
|- SnapshotOfRootViewA
|- SnapshotOfRootViewB, fading in
|- SnapshotOfImageViewA (id: image)
     move from ImageViewA's position to ImageViewB's position
|- SnapshotOfImageViewB (id: image), 
     fading In, move from ImageViewA's position to ImageViewB's position

Note that there is no snapshot created for buttonB because there is no heroModifier specified for it. Hero make this optimization so that it doesn’t create snapshot for all the views in your view hierarchy. (It will be crazy slow if Hero does that)

Instead, buttonB’s visual appearance is captured inside SnapshotOfRootViewB. Hero also temporarly hide ImageViewB when creating the snapshot for RootViewB, so that ImageViewB is not captured inside SnapshotOfRootViewB.

As you can see, SnapshotOfImageViewB is above SnapshotOfRootViewB during the transition, and the appearance of buttonB will be covered by SnapshotOfImageViewB if SnapshotOfImageViewB moves to the location of buttonB.

Lets visit another case. What if we give buttonB a heroModifier?

RootViewA
|- imageViewA (id: image)

RootViewB
|- imageViewB (id: image)
|- buttonB (modifier: .fade)

During the transition. it will be come this:

Container View
|- SnapshotOfRootViewA
|- SnapshotOfRootViewB, fading in
   |- SnapshotOfButtonB, fading in
|- SnapshotOfImageViewA (id: image)
     move from ImageViewA's position to ImageViewB's position
|- SnapshotOfImageViewB (id: image)
     fading In, move from ImageViewA's position to ImageViewB's position

By default, Hero use local coordinate space outlined here: Coordinate Space. Therefore SnapshotOfButtonB is inserted into SnapshotOfRootViewB during the transition. Still not able to solve the problem since it is still behind SnapshotOfImageViewB

ImageViewB, on the other hand, uses global coordinate space because it is matched by a heroId. Views matched by heroId is forced to use global coordinate space, otherwise it will be affected by its superview’s animation which might completely mess up the visual. There is also a couple of other issues, but it is out of scope to talk about in this post.

To actually solve the problem, we need to use .useGlobalCoordinateSpace

RootViewA
|- imageViewA (id: image)

RootViewB
|- imageViewB (id: image)
|- buttonB (modifier: .fade, .useGlobalCoordinateSpace)

During the transition. it will be come this:

Container View
|- SnapshotOfRootViewA
|- SnapshotOfRootViewB, fading in
|- SnapshotOfImageViewA (id: image)
     move from ImageViewA's position to ImageViewB's position
|- SnapshotOfImageViewB (id: image)
     fading In, move from ImageViewA's position to ImageViewB's 
|- SnapshotOfButtonB, fading in

Hooray! ButtonB is finally above ImageViewB.

Note that the view order is depending on the view’s position in its initial view.

If your initial view is like this:

RootViewB
|- buttonB (modifier: .fade, .useGlobalCoordinateSpace)
|- imageViewB (id: image)

then Hero will produce:

Container View
|- SnapshotOfRootViewA
|- SnapshotOfRootViewB, fading in
|- SnapshotOfButtonB, fading in
|- SnapshotOfImageViewA (id: image)
     move from ImageViewA's position to ImageViewB's position
|- SnapshotOfImageViewB (id: image)
     fading In, move from ImageViewA's position to ImageViewB's