Storyboards have been a big part of iOS development since iOS 5 in 2011, but some people have never got on with them. The main reason to not like them, that everyone can surely agree on, is the difficulty of merging storyboards in source control when conflicts arise and arise they will… unless you’re working solo of course, in which case you’ve quite possibly never had to merge much of anything!
Personally I’ve really liked storyboards. I started iOS app development in 2012 and my first project was able to use storyboards so it’s how I learned to do iOS UI. I really like the quick navigational overview you can get from a storyboard. Most of the UI is laid out in a fairly recognizable manner such that you can spot which screen is which, how you can get to them and where you can go from them.
I’ve just covered the topic of reducing storyboard merge hell at the wonderful iOSDevUK 9 conferences. I outlined some ways of reducing and dealing with storyboard merge conflicts and figured a write up of the talk would be beneficial for those who attended and those who didn’t.
So what can we do?
Solution 1: Use SwiftUI
SwiftUI is Apple’s new declarative UI framework being introduced in iOS 13 this year. With SwiftUI Apple seem to be steering away from storyboards (the default app template for a SwiftUI app doesn’t include a storyboard, at least up to Xcode 11 beta 6). You can still use a storyboard and SwiftUI together if you wish to but Apple is definitely steering you one way or the other as the new project wizard asks you if you want SwiftUI or storyboards.
SwiftUI is a bit like an interface builder on steroids. Instead of using a .xib file to define your UI with interface builder’s WYSIWYG editor you define your UI with some really minimal Swift code, which gives you the WYSIWYG preview where you can also make changes that instantly get reflected back to your Swift code. This is great because part of the problem with storyboards (that I’ll get into a bit more later on) is that they’re XML and that’s harder to resolve conflicts in than it is with code.
Alas, SwiftUI requires iOS 13 and for most of our projects, we probably won’t be able to target iOS 13 as our minimum version for another year or so. Maybe a brand new project could target iOS 13 as the minimum but existing projects might not even make sense to migrate to SwiftUI or might need to keep supporting iOS 12 and below for a little longer.
So what else can we do?
Solution 2: No storyboards
This is effectively a great solution to the problem of resolving merge conflicts in storyboards. If you don’t use storyboards you’ll never have to merge them! But storyboards do have some great benefits that we’ll end up losing. Here are two alternatives to using storyboards:
Just use nibs
Instead of using storyboards you can go back to using nibs (this was how UI was laid out visually in interface builder before storyboards came along in iOS 5). Unfortunately, you lose the lovely navigational overview that storyboards give you and you’ll still be open to similar merge conflicts as .xib files are just like small storyboards (though there will be less merge conflicts potentially as you’ll have separated your UI out into more files).
Just use code
If nibs aren’t the perfect alternative, maybe we don’t use them either? Just define all your UI in code. Some people do this and it will certainly avoid the nasty merge conflicts of storyboard/xib files. But with this approach, you’ve lost all visual overview that the WYSIWYG editors give you and you need to run your app and navigate around it to see any UI changes you’ve made.
Solution 3: Diff different
A different diff tool
Whatever source control you’re using, the diff tool is probably ‘line-based’, which means it assumes a single line of the file is the smallest meaningful unit to be compared. For code, this is generally fine but for XML files (e.g. storyboards) it’s not the case as an entire element is the smallest meaningful unit. This makes the merging of changes in XML problematic and results in invalid XML if you’re not careful.
There are various diff tools out there that are ‘XML-aware’ so it could be worth trying to find one that would work well for storyboards. I did a bit of nosing around for something that might work but really didn’t find anything particularly useful. Everything I found seemed to be either very out of date (though maybe that’s not a major issue as XML isn’t exactly evolving), not freely available on macOS or had various issues with it that meant you probably couldn’t rely on it.
I did find a tool called StoryboardMerge, which is a custom diff/merge tool that is specifically designed for storyboards and getting round the difficulties of merging them. Unfortunately, it’s not been touched in four years and given that storyboards can change in each version of Xcode it’s probably not that surprising that it suffers from crashes when trying to use it.
Altering your storyboard
Another option for ‘diffing differently’ is to manipulate your storyboard’s XML into a format that will work better with the line-based diffing tool you might be stuck with.
Below is a simple example of how an xml file can be difficult to merge for a trivial situation, such as two people adding a different attribute to the same element.
Person 1 has added ‘attr2’ to the element and person two has added ‘attr3’. We probably want the merge result to add both attributes, but the default action of accepting both changes results in what we see at the bottom of the image. The entire line has been duplicated, which results in breaking the XML format as the first element doesn’t get closed.
Below is the same change being made but with the xml reformatted so that each element and attribute is on its own line.
We get exactly what we want now, both attributes are added to the element and the XML format is valid.
You could write a pre-commit script that would reformat all your XML files this way and your merging might just get easier! Git has something called ‘hooks’ and Perforce has something called ‘triggers’, which are both sets of scripts that can be run at certain stages of the source control pipeline, so would facilitate this idea.
Solution 4: Be smart
A lot of merge conflicts in storyboards can be avoided by simply being a bit smarter about how you use them.
Keep to yourself
Avoid working on the same parts of a storyboard as someone else to reduce the chances of conflicts. For example, you can get away with minimal issues if you stick to editing different view controllers to other people. This won’t stop all conflicts as many can still occur (some of which I'll cover shortly), but it does help.
This just requires communication. When you’re planning your work allocate it and schedule it such that multiple people don’t need to edit the storyboard all at once. If you do have some crossover then talk to each other to make sure you’re not editing it while someone else is and make sure you get the latest version from your source control before starting your changes (to hopefully avoid any merging at all) and get them committed as soon as you can (nice small atomic commits, often, right?).
Lock your storyboards
Some source control systems support locking of individual files so that only one person can edit them at a time.
Github doesn’t seems to have file locking (though their large file system does), GitLab seems to have some support and Perforce certainly does. SVN also seems to support it… does anyone still use that..?
It’s probably not practical to lock your storyboards if you’ve only got one file for all your UI, but it might be more practical if you’ve broken it down into multiple chunks.
Many projects have a single storyboard for the whole UI. For a small app, this is fine but for larger apps, it can be a problem in terms of performance/practicality of viewing/editing the storyboard and will lead to an increased chance of merge conflicts. But you don’t need to have a single storyboard, you can break up your UI across multiple storyboards.
Xcode even has the ability to do this for you: Editor > Refactor into a storyboard and you can still segue between view controller in different storyboards with a ‘storyboard segue’.
We don’t use a single file for all our code so why put all the UI in one file? Well, sure it gives you that lovely navigational overview, but maybe there are a number of separate flows in your app that don’t really need to be seen all at the same time. Maybe you’ve got a sign-up flow that could be in its own storyboard or you’ve got a tab controller that could have each of its individual tabs flows in a separate storyboard.
The more storyboards you have, the less chance of merge conflicts you have as there’s less chance of people working on the same files at the same time.
Solution 5: Hybrid storyboards
This is an idea I came up with back in 2017 when I was working at an app development company leading a small team of developers. Often we’d work on apps individually but sometimes we’d come together on the same project and that’s where we’d face the merge issues in storyboards.
Full disclosure here, I’ve not used the following approach in anything more than some little tests so I could talk about it with more confidence. I’ve certainly not made any apps using this approach so there could be some gotchas further down the line that I’ve not considered!
The idea is quite simple. Put all your UI into nibs (.xib interface builder files) but keep the storyboard to define your navigation between screens with segues. It’s almost like breaking down your storyboard so far that you have one screen per storyboard but you get to keep the navigational overview of your app, to a certain extent.
Let’s look at an example. Below is an example of a simple storyboard with a login screen and a home screen.
And below is the hybrid version where the storyboard now has no UI elements (apart from some labels that help us identify the screens now that there’s nothing else there). There are also two nib files, of course, which look just like the screens in the original storyboard above.
All we have to do is load in the correct nib to each of the view controllers when it’s loading up and get rid of the label from the storyboard. There are a few ways to do that, which I’ve outlined in my loading nibs in iOS blog post.
There should be a clear benefit with this hybrid approach in terms of conflicts. Your UI is being separated out into more files so more people can then work on those files without causing conflicts. Diffing a nib file is going to be easier than a storyboard if only because you know it’s just one screen (nibs can have multiple views in them but lets be sensible and keep it to one!), whereas in a storyboard if you’re going between changes or conflicts they could be for any of the multiple screens you’ve got in the storyboard and it’s not always easy to figure out which it is as the XML for each view controller can get very long.
Another benefit is that your storyboard will become more responsive during development. I know in the past when I’ve had a massive (single) storyboard in a project my Mac can start to struggle when loading it and interacting with it.
Triggering a segue becomes a bit more awkward. If your UI is in the storyboard you can easily setup a segue straight from a button or cell but not if that button or cell is in a nib. There may be similar awkwardness, though this one might not be so bad. I often find that I start off with segues being triggered directly from buttons but then end up triggering them by name later on as the app becomes more complex.
This idea went from silver bullet to potentially pointless over the course of my investigations. But there’s certainly some truth to moving things out of storyboard and into nibs, especially if you find yourself duplicating the same UI in different screens!
Solution 6: Just do it
Merging storyboard conflicts is difficult. Part of the problem is that the diff/merge tools being used aren’t well suited to dealing with XML files, another part of the problem is that we don’t often need to interact with the XML of a storyboard so it’s very unfamiliar to and thus not clear how to resolve a lot of the conflicts.
The only time you need to look at the XML (unless you’re curious) is when there are merge conflicts, which if you’re lucky might not happen too often. For a lone programmer, it’s great that you don’t need to know anything about the XML of a storyboard to use them to their full extent. But then it becomes something additional that you need to learn when you become part of a team and get faced with merge conflicts.
Even though I’ve laid out some ways of helping to avoid merge conflicts in storyboards in this article none of these ways really prevent them from happening. So we need to cover how to handle them when they do happen too!
I’ve written a separate blog on the anatomy of a storyboard, where I go into some of the details of the XML format used by storyboards. Give it a read and hopefully, you’ll learn more about the elements and attributes used in storyboards and therefore find them a little easier to merge.
One option to deal with merge conflicts I’ve certainly employed in the past is to wander over to the person who’s made the change I’ve got a conflict with and see if they’re willing to let me obliterate their changes and have them redo them. Cheeky, right? Generally, it’s the easiest solution but often it won’t be practical and it’s also just avoiding the problem so you’re not learning how to deal with it properly!
Let’s look at some further ways to reduce conflicts from appearing, given we know a bit more about the XML of storyboards (because you definitely went away and read my anatomy of a storyboard blog, right?):
Everyone on the same version of Xcode
A common merge conflict I’ve seen in the past happens when people are using different versions of Xcode. Near the top of the XML, the version of the tools is logged so if people are on different versions it can keep changing back and forth and end up in a conflict. Often it’s not a major issue to resolve but you can save yourself that annoyance and make sure everyone can successfully open the storyboard if you’re all on the same version.
Use auto-layout everywhere
If you’re using auto layout everywhere in your storyboards then you get protection against the next two suggestions and could potentially ignore them. Though, having said that, you’ll still get merge conflicts due to these reasons so if you take them on board you’ll be reducing the number of merge conflicts you face.
The reason auto-layout protects you is because the issues below are related to the frames of views being affected by a couple of things, resulting in the possibility of important changes getting lost in merge conflicts.
The reason auto layout protects you from needing the following two suggestions is that the constraints for auto layout define where views should be. If the frames hardcoded in the storyboard XML end up in the wrong place after a merge then they can be automatically fixed by Xcode’s ‘update frames’ button.
Commit with the same preview device
You can change what device your storyboard is showing your screens in, which lets you see how they would look on an iPad versus an iPhone (and all the screen size differences in-between). That’s awesome, isn’t it? But what does changing the preview device do?
Below we see two of the changes that will occur to a storyboard if you change the preview device.
The device tag at the top has changed to reflect which device the storyboard should preview with but the frame of the view in the view controller has also changed. Ok, well it has to be rendered differently so that’s understandable. But imagine you’ve switched the preview device and also made a tweak to some of the frames and then that ends up conflicting with someone else’s changes. How do you know if the frame has changed merely because of the device change or also because of an intentional tweak?
If you’re using auto layout throughout your entire UI then this shouldn’t be much of an issue because any inconsistencies after a merge will be flagged up to you and you can automatically fix them with the ‘update frames’ button. But if you’re not using auto layout everywhere your efforts in resolving merge conflicts could overwrite another person’s intentional changes to a frame.
So it’s probably a good idea to agree with a preview device to stick to across your team, at least when committing the storyboard. You can of course switch between preview devices safely locally but make sure you commit with the agreed preview device selected.
That’s easier said than done of course, and it only takes one person to forget and then you’re all into trouble. To enforce the rule you could setup a pre-commit check in your source control that would reject any commits that have changed the preview device (Git hooks or Perforce triggers can be used for this).
Be very careful with inferredMetricsTieBreakers
If you’ve been dealing with storyboard conflicts already then you’ve probably seen a conflict in the ‘inferredMetricsTieBreakers’ element, right at the end of the storyboard XML. It’s probably not obvious what it means if you’ve not looked it up but you’re sure to have seen these options in Xcode before:
Inferred metrics in the storyboard mean that the context of the view controller can change how it appears. If it’s in a navigation controller it will show a navigation bar but if it isn’t then it won’t.
Storyboard uses the inferredMetricsTieBreakers to determine which context to display a view controller in if it can appear in multiple ways. It all comes down to the segues that lead into the view controller, and that’s what’s being listed in the inferredMetricsTieBreakers element in the XML: segue identifiers.
So here’s an example of how the inferredMetricsTieBreakers might change. You may have a screen that can be accessed via a segue that pushes it onto a navigation controller and it might also be displayed somewhere else in your app modally.
As mentioned above this causes a situation where the screen has a navigation bar in one context but not in the other and Xcode will let you see both those contexts by selecting the different segues. Clicking on the segue between the navigation controller and the screen on the right will show it with a navigation bar, clicking on the segue from the bottom left screen to the one on the right will show it without the navigation bar. The last segue you click on will have its identifier listed in the inferredMetricsTieBreakers element in the storyboard’s XML.
Just clicking on a segue can change a load of frames within that view controller’s view hierarchy because the navigation bar may push those views further down the screen. This can be dangerous because, without auto layout to define the true placement of those views, this could be masking intentional changes to those frames that were also made at the same time.
One solution to this could be to have a pre-commit script which checks you’ve not changed the inferredMetricsTieBreakers in a way that could cause a conflict. A change only needs to occur if you’ve added a new segue (and that segue’s ID appears in the IMTBs) or removed a segue (that used to be in the list). Any other changes mean you’ve potentially changed the layout of a screen in an un-meaningful way and could cause a difficult merge so you should go back to the storyboard and select the segues in such a way to avoid this.
This sounds pretty painful to get right as the script would need to be more complex than I’ve outlined and then going to sort out any issues raised might be tricky too (though the script could be written in such a way that it tells you exactly which segue(s) to go and select).
After you’ve merged get Xcode to auto-fix little mistakes
When Xcode loads up a storyboard it may find some of your manual tinkering in the XML was a little incorrect and helpfully fix it for you, which is nice! You can also check for any warnings about misplaced frames and auto-fix them with the ‘update frames’ button (if you’re using auto-layout).
Xcode will also tell you if you’ve completely broken the storyboard file and it’s unable to load it, in which case it would be inadvisable to commit to source control and you need to reattempt the merge!
More articles on merging storyboards
With a combination of the tips, I’ve provided here your day to day merge issues in storyboards should hopefully be reduced to a generally manageable level.
But there’s still an opportunity to merge hell. As I mentioned earlier you should make sure you commit changes in your storyboards regularly and avoid people working on them at the same time. But what if you’ve got multiple branches of your project in source control, being worked on at the same time?
Perhaps you’ve made a branch of your project so you can rework your UI to a new design. You branched because it’s going to take some time and you don’t want to prevent other releases of your project going out in the meantime. Once you’ve finished your work you’ll have loads of changes in the storyboard and the main branch has continued moving forward, making changes to the storyboard to add new features or fix bugs and maybe those changes are all needed in the new UI too. That could be a really nasty merge you’re faced with and lots of important work could be lost if you do it wrong.
One of the important things to have, when working with branches, is a strong branch strategy for dealing with things like this. Often this will mean that you regularly update your branches from the main branch so that the eventual merge back won’t have to do it all in one go. But even if you catch up your branch daily you could still be faced with some nasty merges. Maybe you need to plan for the storyboard pain and whenever there are changes on the main branch make sure you bring them across straight away while they’re still fresh in people’s minds.
Sometimes you might need to accept that a merge isn’t going to be a quick job that you can do in the merge tool. Perhaps you’ll need to view all three storyboards in Xcode to see what the changes look like visually to understand what’s happened.
I’d love to hear any suggestions you might have for dealing with storyboard merge conflicts or any thoughts you have on what I’ve covered here!