Fixing UI Elements that Float Away

While testing our iOS app, my team found a puzzling bug—repeatedly clicking some of the buttons on the main dashboard caused the whole row of buttons to gradually drift up or down, even off the bottom of the screen. It didn’t always move, but once it started moving, it tended to keep sliding in the same direction.

We were building the UI with Auto Layout constraints so it would be responsive to phones with different screen sizes. This meant that we didn’t have a lot of visibility into how the precise sizes of different parts of the UI were calculated. We specified that the row should span from 85% down the screen to the bottom and that other elements around it should shrink to ensure it had enough space. The layout engine handled the details.

The bug appeared when testing on iOS 7 but not iOS 8, which suggested the root cause might be in the constraint layout library. Since the positioning started out correct but gradually drifted, I wondered if floating point error was involved.

Floating Point Errors

Just like 1/3 can’t be represented exactly by 0.3333333… without an infinite number of ‘3’s, many numbers that are easy to represent in decimal can only be approximated by binary floating point numbers. If these rounded numbers are fed back into a formula and re-rounded several times, they can drift far enough to cause strange bugs.

For example, once I saw a CAD drawing in architectural design software glitching because of floating point error. As the designer was setting up their drawing, they had accidentally imported CAD data at the wrong scale. They removed it and continued working without noticing (or being warned, a UX bug) that they were working on parts of their layout that were millions of kilometers away from the origin. When they rendered 3D views of their building, inconsistent rounding led to Cubist takes on their design. It was too late—working beyond the outer limits of accuracy added random turbulence, and the original positioning couldn’t be recovered.

Finding & Fixing the Bug

With this in mind, we reviewed the layout rules again. 0.85 stood out—while it looks like a “round” number, it’s impossible to represent exactly in floating point. 0.85 was becoming something more like 0.85000002384185791016, and that small amount of difference seemed to grow like compound interest as the UI elements shifted each other around to fit the constraints. If this was an Auto Layout bug, we could work around it by using numbers that could be represented precisely.

I wrote a quick Python script to print out close numbers that could be used, the sums and differences of reciprocals of powers of two: 0.75 is 1/2 + 1/4, 0.625 is 1/2 + 1/4 – 1/8, and so on. IEEE 754 floating point numbers are represented in binary with a sign bit (+ or -), an 8-bit exponent, and a 23-bit fraction. 0.75 has a sign bit of 0 (positive), an exponent of 126 (-1 for 0.5, but offset by 127 instead of having another sign bit), and 1 bit at the top of the fraction to represent +1/4. -1/3 is approximated by a sign bit of 1 (negative), an exponent of 127 – 2 (it’s between 0.25 and 0.5), and a fraction of 0x19999a. The fraction part has lots of bits set, and ultimately has to round up to 0xa (10) rather than 0x199999999... going on forever.

We decided on 0.8125, which was close enough to 0.85, and confirmed that the drifting stopped. (With an appropriate comment explaining why we chose such a weird number.) We also noticed that numbers that could be represented using double-precision floating point numbers (which use 64 bits instead of 32, for more accuracy) but not single-precision didn’t help, so apparently iOS 7 Auto Layouts was using single-precision somewhere.

Conversation
  • Ken Fox Ken Fox says:

    I wonder if you had an under-constrained layout and the solver was finding different solutions for different initial positions? Apple uses the Cassowary algorithm–I bet you could use theft to search for layout bugs due to constraint issues.

  • Comments are closed.