There’s no denying that Storyboards are an amazing addition to our App development toolset, but there seems to be a clear divide between developers who enjoy them and those who do not. And it seems that those who don’t are developers who work collaboratively on teams. For example, Adam Ernst, an iOS Engineer for Facebook, was asked at the Mobile DevCon on April 13th, 2013, in New York after presenting How We Built Facebook for iOS on how they handle .xib and .xcodeproj merging. His answer was pretty much the same as most others I’ve seen online:
Don’t use xibs is the answer. Not only because they’re not mergeable, but they’re a pain to deal with localization. So, all your interfaces and everything are in code. And yeah, Xcode projects are not easily mergable. We just deal with the pain. We have merge conflicts once in a while, sadly.
– Adam Ernst, Facebook
Curiosity struck me. How hard could it be? I decided to have a look at why this has become the generally accepted answer. Storyboards at their core are just XML files, right? Here’s an example blank Storyboard file after adding it to a project:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="2.0" toolsVersion="3084" systemVersion="12D78" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="2083"/> </dependencies> <scenes/> <simulatedMetricsContainer key="defaultSimulatedMetrics"> <simulatedStatusBarMetrics key="statusBar"/> <simulatedOrientationMetrics key="orientation"/> <simulatedScreenMetrics key="destination" type="retina4"/> </simulatedMetricsContainer> </document> |
You can view the source code of your own Storyboard by right clicking the file in the Project Navigator and clicking “Open As > Source Code”. Now, let’s take a very simple problem and see how complex this can get.
The Scenario:
You have a team of 2 developers, each of whom is tasked with managing a simple View Controller within a shared Tab Bar Controller. For the simplicity of this problem, there will be nothing but a single View within each controller, and a Label stating which view is being displayed. The tabs are arranged from left to right, Developer A and Developer B respectively. Developer A’s View background must be colored Green, and Developer B’s View background must be colored Blue. Each developer will leave a blank View Controller in place of the other’s, which will aide in the merging process (I hope).
Merging with Storyboards:
Developer A’s Storyboard:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="2.0" toolsVersion="3084" systemVersion="12D78" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="w3G-aj-ISz"> <dependencies> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="2083"/> </dependencies> <scenes> <!--View Controller - Developer A--> <scene sceneID="x7x-Ec-5g3"> <objects> <viewController id="zt5-tS-Lu7" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="Y21-ht-nSE"> <rect key="frame" x="0.0" y="20" width="320" height="499"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <subviews> <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Developer A" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xEe-94-zZL"> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> <nil key="highlightedColor"/> </label> </subviews> <color key="backgroundColor" red="0.50196081399917603" green="1" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> <constraints> <constraint firstAttribute="trailing" secondItem="xEe-94-zZL" secondAttribute="trailing" constant="20" symbolic="YES" type="default" id="E66-Ec-ET3"/> <constraint firstItem="xEe-94-zZL" firstAttribute="centerY" secondItem="Y21-ht-nSE" secondAttribute="centerY" type="default" id="L9k-fw-wZT"/> <constraint firstItem="xEe-94-zZL" firstAttribute="leading" secondItem="Y21-ht-nSE" secondAttribute="leading" constant="20" symbolic="YES" type="default" id="far-bo-TSb"/> </constraints> </view> <tabBarItem key="tabBarItem" title="Developer A" id="JAB-wZ-u7u"/> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="xb9-Hb-peD" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="713" y="-82"/> </scene> <!--View Controller - Item--> <scene sceneID="M6O-Lb-ngs"> <objects> <viewController id="dMd-u2-nqA" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="GjY-HT-Rg7"> <rect key="frame" x="0.0" y="20" width="320" height="499"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> </view> <tabBarItem key="tabBarItem" title="Item" id="7iL-gU-GSc"/> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="rVh-jR-3wd" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="713" y="631"/> </scene> <!--Tab Bar Controller--> <scene sceneID="swm-Yd-Coe"> <objects> <tabBarController id="w3G-aj-ISz" sceneMemberID="viewController"> <toolbarItems/> <nil key="simulatedBottomBarMetrics"/> <tabBar key="tabBar" contentMode="scaleToFill" id="ob2-3T-4an"> <autoresizingMask key="autoresizingMask"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> </tabBar> <connections> <segue destination="zt5-tS-Lu7" kind="relationship" relationship="viewControllers" id="ANE-LM-eNy"/> <segue destination="dMd-u2-nqA" kind="relationship" relationship="viewControllers" id="OLX-72-PJD"/> </connections> </tabBarController> <placeholder placeholderIdentifier="IBFirstResponder" id="THR-0M-0BJ" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="157" y="270"/> </scene> </scenes> <simulatedMetricsContainer key="defaultSimulatedMetrics"> <simulatedStatusBarMetrics key="statusBar"/> <simulatedOrientationMetrics key="orientation"/> <simulatedScreenMetrics key="destination" type="retina4"/> </simulatedMetricsContainer> </document> |
Developer B’s Storyboard:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="2.0" toolsVersion="3084" systemVersion="12D78" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="HZS-FD-PAm"> <dependencies> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="2083"/> </dependencies> <scenes> <!--View Controller - Developer B--> <scene sceneID="3AM-2v-eXm"> <objects> <viewController id="Zzk-0o-YgM" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="f04-uC-XIj"> <rect key="frame" x="0.0" y="20" width="320" height="499"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <subviews> <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Developer B" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Auu-dR-mBh"> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> <nil key="highlightedColor"/> </label> </subviews> <color key="backgroundColor" red="0.0" green="0.50196081399917603" blue="1" alpha="1" colorSpace="calibratedRGB"/> <constraints> <constraint firstItem="Auu-dR-mBh" firstAttribute="centerY" secondItem="f04-uC-XIj" secondAttribute="centerY" type="default" id="5iG-tk-fUi"/> <constraint firstAttribute="trailing" secondItem="Auu-dR-mBh" secondAttribute="trailing" constant="20" symbolic="YES" type="default" id="Mak-2N-h88"/> <constraint firstItem="Auu-dR-mBh" firstAttribute="leading" secondItem="f04-uC-XIj" secondAttribute="leading" constant="20" symbolic="YES" type="default" id="ons-v6-YgU"/> </constraints> </view> <tabBarItem key="tabBarItem" title="Developer B" id="GGv-TJ-aYB"/> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="EA6-rF-cCW" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="625" y="605"/> </scene> <!--Tab Bar Controller--> <scene sceneID="euI-7P-JOm"> <objects> <tabBarController id="HZS-FD-PAm" sceneMemberID="viewController"> <toolbarItems/> <nil key="simulatedBottomBarMetrics"/> <tabBar key="tabBar" contentMode="scaleToFill" id="J4V-bj-CtS"> <autoresizingMask key="autoresizingMask"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> </tabBar> <connections> <segue destination="OF6-7M-QQx" kind="relationship" relationship="viewControllers" id="WWr-ML-jGh"/> <segue destination="Zzk-0o-YgM" kind="relationship" relationship="viewControllers" id="lYs-VP-xGE"/> </connections> </tabBarController> <placeholder placeholderIdentifier="IBFirstResponder" id="UIh-H2-osv" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="85" y="265"/> </scene> <!--View Controller - Item--> <scene sceneID="6fx-RO-fyg"> <objects> <viewController id="OF6-7M-QQx" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="l7m-Bj-F1d"> <rect key="frame" x="0.0" y="20" width="320" height="499"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> </view> <tabBarItem key="tabBarItem" title="Item" id="eay-rB-8Ll"/> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="ADx-xD-cZW" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="631" y="-114"/> </scene> </scenes> <classes> <class className="NSLayoutConstraint" superclassName="NSObject"> <source key="sourceIdentifier" type="project" relativePath="./Classes/NSLayoutConstraint.h"/> </class> </classes> <simulatedMetricsContainer key="defaultSimulatedMetrics"> <simulatedStatusBarMetrics key="statusBar"/> <simulatedOrientationMetrics key="orientation"/> <simulatedScreenMetrics key="destination" type="retina4"/> </simulatedMetricsContainer> </document> |
Xcode provides a utility for merging files called FileMerge, located in the Xcode menu under “Xcode > Open Developer Tool > FileMerge” (also located at /Applications/Xcode.app/Contents/Applications/FileMerge.app). Using this utility, we can identify 17 differences.
Each of the differences can be selected individually and an appropriate Action selected from the bottom Right corner. Let’s attempt to resolve these differences 1-by-1:
- Although the initialViewController for each Storyboard is properly set, the IDs of respective view controllers are different in each file. Because the initialViewController is Developer A’s, we’ll set this to “Chose Left”.
- We’ve reached a point of contention. It seems that FileMerge doesn’t understand that these XML elements are suposed to be different objects, and instead would like us to override the other View Controller’s information. This is not what we want to do at all …
… /facepalm …
So, What Happened?
Well, this is the point where we come to a crude discovery that Storyboard XML files are not hierarchal, and Merge/Diff utilities like FileMerge will not help to resolve this problem. In fact, as of this writing, I don’t know of any utility that will make any of this easy to do. If you you, feel free to drop a comment below and shed some insight onto this problem. As an independent developer, I’ve never found myself in a situation where I would actually need to do something like this. This experiment was purely created for the sake of exploring the difficulty of merging Storyboard files, and left here for your educational entertainment.
Moral of the Story (TL;DR)
If you are part of an iOS development team, handcode all of your UI elements.


