DEV Community

swainwri
swainwri

Posted on

Legacy Mini Split Views in UIKit

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)