Why I Stopped Fighting UISplitViewController and Built My Own
“Storyboards aren’t the problem. Implicit storyboards are.”
This article documents a real-world, multi-hour debugging session that ended with a clean, predictable UIKit architecture — and a small demo repo you can learn from.
Repo: 👉 https://github.com/swainwri/LegacyMiniSplitDemo
If you’ve ever:
• Fought floating detail controllers
• Had navigationController == nil when it “shouldn’t be”
• Watched UIKit silently replace your view hierarchy
• Or wondered why Split View behaves differently every OS release…
This is for you.
⸻
The problem we were trying to solve
I had a legacy UIKit app that needed:
• A master / detail layout on iPad
• A **st…
Why I Stopped Fighting UISplitViewController and Built My Own
“Storyboards aren’t the problem. Implicit storyboards are.”
This article documents a real-world, multi-hour debugging session that ended with a clean, predictable UIKit architecture — and a small demo repo you can learn from.
Repo: 👉 https://github.com/swainwri/LegacyMiniSplitDemo
If you’ve ever:
• Fought floating detail controllers
• Had navigationController == nil when it “shouldn’t be”
• Watched UIKit silently replace your view hierarchy
• Or wondered why Split View behaves differently every OS release…
This is for you.
⸻
The problem we were trying to solve
I had a legacy UIKit app that needed:
• A master / detail layout on iPad
• A stacked navigation flow on iPhone
• Predictable behavior across iOS versions
• Zero UIKit “magic”
• No SwiftUI
• No new navigation frameworks
In theory, this is exactly what UISplitViewController is for.
In practice… it wasn’t.
⸻
What went wrong with UISplitViewController
Over the course of hours, we hit all the classics: • Floating detail controllers instead of tiled layouts • Master views disappearing • Detail views appearing twice • Navigation stacks duplicating • showDetailViewController doing different things per device • preferredDisplayMode, preferredSplitBehavior, style… none behaving consistently • iPhone collapsing rules leaking into iPad behavior • Storyboards loading even when you thought they weren’t
The core issue wasn’t misuse.
The core issue was loss of control.
UIKit was deciding too much for us.
⸻
The real root cause (this is important)
The actual failure mode was a combination of: 1. Implicit storyboard loading 2. Implicit Split View lifecycle 3. Navigation controllers created behind your back
Even when you think you’re “configuring” Split View, UIKit has already made decisions you can’t undo.
At some point the question became:
Why am I fighting a controller that exists to make decisions for me?
⸻
The pivot: stop using UISplitViewController entirely
Instead of bending UIKit to behave like a legacy split view…
We built one.
Not with hacks. Not with layout tricks. Just honest UIKit.
⸻
The new architecture
One custom container:
LegacySplitViewController
It owns: • A master container view • A detail container view
That’s it.
No Split View APIs. No collapsing rules. No adaptive magic.
Just two child navigation controllers.
⸻
Storyboards: yes — but intentionally
This project does use storyboards, but very deliberately: • Main_iPad.storyboard • Main_iPhone.storyboard
And crucially:
❌ No Main.storyboard ❌ No UIMainStoryboardFile ❌ No UISceneStoryboardFile
in Info.plist.
Storyboards are loaded explicitly in SceneDelegate, based on device idiom.
This is the key difference.
⸻
SceneDelegate: where control lives again
if UIDevice.current.userInterfaceIdiom == .pad {
window.rootViewController = makePadRoot()
} else {
window.rootViewController = makePhoneRoot()
}
This one decision: • Eliminates ambiguity • Prevents UIKit auto-loading • Makes navigation reasoning trivial again
⸻
iPad behavior (manual, predictable)
On iPad:
• LegacySplitViewController is root
• It contains:
• masterNav
• detailNav
• Selecting an item replaces the detail navigation stack
• The master never disappears unless you hide it
No floating. No duplication. No “why is this a popover”.
⸻
iPhone behavior (stacked, natural)
On iPhone:
• No split container
• A single UINavigationController
• Selecting an item simply pushes
Same code paths. Different roots. Zero conditionals deep in your view controllers.
⸻
Navigation logic (simplified at last)
From the master:
if UIDevice.current.userInterfaceIdiom == .pad {
detailNav?.setViewControllers([vc], animated: false)
} else {
masterNav?.pushViewController(vc, animated: true)
}
That’s it.
No showDetailViewController. No guessing what UIKit will do. No inspecting parent chains at runtime.
⸻
The big lesson
The mistake wasn’t “using UISplitViewController wrong”.
The mistake was assuming it was still the right abstraction for: • Legacy UIKit apps • Explicit navigation • Predictable behavior • Multi-year maintenance
For new SwiftUI apps? Fine.
For UIKit apps that must remain sane?
A manual split is often better.
⸻
What’s intentionally missing
This demo does not include: • Sidebar collapse/expand gestures • Interactive resizing • Fancy adaptive transitions
Those can be added — now that the foundation is solid.
The point of this repo is correctness first.
⸻
Final takeaway
If you remember one thing:
UIKit works best when it does less, not more.
By removing: • Implicit storyboards • Implicit split behavior • Implicit navigation rules
…we ended up with simpler code, not more.
⸻
Repo
👉 https://github.com/swainwri/LegacyMiniSplitDemo
Clone it. Break it. Improve it.
And next time UIKit gaslights you — build the container yourself.