Interface Builder degrades storyboards, resizes and repositions views in small increments
We have a number of iOS apps that several different developers contribute to. A problem that I continue to notice is that views in our storyboards will shift out of the position they were put in or resize so that they are smaller, which on labels that were sized to fit text originally becomes painfully obvious when the labels all of a sudden are truncating their text.
I'm noticing these degradations of our views appear in commits to our Git repository when the developer did not directly make any edits to the storyboard. They may have viewed the storyboard in Interface Builder, but did not make any real changes to the storyboard. The changes were nevertheless saved and committed along with what they were working on.
When I do a text compare between the storyboard files before and after the responsible commits I see small changes to view frames such as:
<rect key="frame" x="203" y="8" width="362" height="29"/>
|
V
<rect key="frame" x="203" y="7.5" width="362" height="29"/>
and
<rect key="frame" x="446.00000170260091" y="7" width="302" height="30"/>
|
V
<rect key="frame" x="446" y="7" width="302" height="30"/>
and
<rect key="frame" x="364" y="3" width="200" height="38"/>
|
V
<rect key="frame" x="363" y="3" width="200" height="38"/>
and
<rect key="frame" x="284" y="7" width="97" height="30"/>
| |
V V
<rect key="frame" x="283" y="7" width="96" height="30"/>
and
<rect key="frame" x="384.00001078580522" y="7" width="101" height="30"/>
| |
V V
<rect key="frame" x="383.00000530853856" y="7" width="100" height="30"/>
Most of the time the numbers for frame dimensions are changing by just a small amount, either an integer value changes by one or a floating point value is truncated or the decimal portion is changed minorly.
Other times, the values are changing by a few points though like:
<rect key="frame" x="334" y="3" width="200" height="38"/>
|
V
<rect key="frame" x="331" y="3" width="200" height="38"/>
and
<rect key="frame" x="251" y="7" width="223" height="30"/>
|
V
<rect key="frame" x="251" y="7" width="220" height="30"/>
and
<rect key="frame" x="478" y="3" width="274" height="38"/>
| |
V V
<rect key="frame" x="475" y="3" width="276" height="38"/>
Note that all of these example frame changes were taken from the same example commit when the developer did not intend to make a single change to the storyboard. There were 269 differences in the XML between the two versions of the file, all of them being these slight changes in frame sizes or positions. The storyboard XML is ~9000 lines.
It seems the issue may have something to do with IB's use of floating point numbers and rounding errors and the differences that become off by a few pixels could be an aggregation of these rounding errors over a period of several times opening, parsing and re-serializing the data.
This is just a theory though as I've not been able to pinpoint the exact cause of the unwanted changes. Often commits don't make any significant changes to the frames at all, only insignificant floating point changes such as 446.00000055262581 -> 446.00000112002783. But when the serious changes occur, they seem to occur in large numbers.
The commits between which the changes occur are also made by the same developer using the same version of Xcode and Interface Builder too. In this example commit where this data was taken, the document tag is <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A389" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" initialViewController="JAD-vj-VfC">
in both versions of the storyboard file for example.
Other than being sure to check not to commit insignificant or unintended changes to storyboard files, I'd like to narrow down what is causing these unwanted changes to our storyboard views. If it's something we can avoid doing that is causing the issue, we can be aware of the cause.
Update: As Tim helpfully noted, this issue does seem to be caused while using Interface Builder on a retina display. All the developers that have caused the problem have retina MacBook Pros. Those of us without retina displays have not experienced the issue.
The most interesting clue in this mystery is that it seems especially bad when you open the same storyboard on a Retina display as opposed to a non-retina display.
At first I was going back and forth between a 4k iMac and a pre-retina Macbook pro and was getting a large volume of changes (~300 lines altered each time).
Then I simply dragged the xcode window from my main monitor (4k/retina) over to my second monitor (2560x1440, non-retina) - and while the window was the same size, xcode resized all the elements and complained of ~50 misplaced views. I moved it back to the retina display and about half of the "misplaced" errors went away, but half remained. The re-scaling - as you suggest - degrades the underlying data.
If you've got multiple developers working on the same file, this is bound to happen frequently.
The solution? This is likely all on Apple to correct - I haven't run into any settings that would mitigate it otherwise.
This seems to be a bug related to Interface Builder's serialization of CGFloat values. The size and position values for view frames are floating points. But their values are always integers. The internal graphic manipulation requires them to be floating point for the transform math to work, but everything is always expressed in terms of round integer point values.
When these floating point values are serialized to the storyboard XML, IB often serializes the values as integers, but occasionally serializes them as floating point numbers too. I'm not sure why it makes the decision to do it this way when it does, but it's less common. In my example frames above, 3 of the values ended up being floating point, while the others are integers.
Also seen in my examples, often the floating point representation will be changed to be serialized as an integer instead. This is where I believe the bug is found.
One thing I noticed with the way view frames were changing, they were tending to move to the left or shrink in size. So the values are mostly getting smaller. You can see in the examples I provided, this is most always the case.
Floating points don't have the precision to express integer values exactly, but are accurate out to several decimal places. So while sometimes integers are expressed as those in my examples as slightly higher than the integer value (i.e. 384.00001078580522), others are expressed as slightly lower than the integer value. Here's an example of a frame change that IB made:
<rect key="frame" x="457" y="7" width="291" height="30"/>
|
V
<rect key="frame" x="456.99999985252464" y="7" width="291" height="30"/>
While this particular change didn't seem to modify the frame's value directly. Both numbers are essentially equal to 457. What I think is occurring is when this XML is re-parsed when the storyboard is opened again, it could be truncating the 456.99999985252464 value and reading it as 456. This is then causing the values to get progressively smaller, shrinking the sizes or shifting the position of the frames to the left or up.
Of course this is just a theory and doesn't give a reason for why Interface Builder is doing this. It seems to have begun since the recent Xcode 6 release. Also, it doesn't explain how it went from 8 to 7.5 on one example or even the one time it went from 274 to 276 in my last example. But for the most part, most of the changes tend to be in the downward direction.
I'm filing a bug with Apple to have this looked into.
I might have an answer to this problem. Its a lesser know feature of opening the apps in low resolution mode. We recently had a similar problem wherein the content view of the table view cells had a 0.5 extra for the height when the separator line is set to default or single. When its set to None then this problem was not there. Steps 1. Drag a default TVC to storyboard. Check the height of the content view of the table view cell. It will be 43.5. 2. Set the separator line for the table view to None. The content view of the cell changes to 44.
Now quit Xcode and set the Open in Low Resolution mode in Finder Get Info window for the Xcode app. Now if you follow the same steps above it will show the height of the content view for table view cell as 43.
When there are different team members working on retina and non-retina displays you would simply get the storyboard files as modified just because you opened the storyboard in retina display. One workaround for this is to turn the Open in Low Resolution mode and work. But then it defeats the purpose of having a retina display though but better than having storyboards marked as modified even though you have not changed anything.