SplitView
A flexible way to split SwiftUI views with a draggable splitter
Install / Use
/learn @stevengharris/SplitViewREADME
SplitView
The Split, HSplit, and VSplit views and associated modifiers let you:
- Create a single view containing two views, arranged in a horizontal (side-by-side)
or vertical (above-and-below)
layoutseparated by a draggablesplitterfor resizing. - Specify the
fractionof full width/height for the initial position of the splitter. - Programmatically
hideeither view and change thelayout. - Arbitrarily nest split views.
- Constrain the splitter movement by specifying minimum fractions of the full width/height for either or both views.
- Drag-to-hide, so when you constrain the fraction on a side, you can hide the side when you drag more than halfway beyond the constraint.
- Prioritize either of one the views to maintain its width/height as the containing view changes size.
- Easily save the state of
fraction,layout, andhideso a split view opens in its last state between application restarts. - Use your own custom
splitteror the default Splitter. - Make splitters "invisible" (i.e., zero
visibleThickness) but still draggable for resizing. - Monitor splitter movement in realtime, providing a simple way to create a custom slider.
Motivation
NavigationSplitView is fine for a sidebar and for applications that conform to a nice master-detail type of model. On the other hand, sometimes you just need two views to sit side-by-side or above-and-below each other and to adjust the split between them. You also might want to compose split views in ways that make sense in your own application context.
Demo
This demo is available in the Demo directory as SplitDemo.xcodeproj.
Usage
Install the package.
- To split two views horizontally, use an HSplit view.
- To split two views vertically, use a VSplit view.
- To split two views whose layout can be changed between horizontal and vertical, use a Split view.
Note: You can also use the .split, .vSplit, and .hSplit view modifiers that come
with the package to create a Split, VSplit, and HSplit view if that makes more sense to you.
See the discussion in Style.
Once you have created a Split, HSplit, or VSplit view, you can use view modifiers on them to:
- Specify the initial fraction of the overall width/height that the left/top side should occupy.
- Identify a side that can be hidden and unhidden.
- Adjust the style of the default Splitter, including its color and thickness.
- Place constraints on the minimum fraction each side occupies and which side should be prioritized (i.e., remain fixed in size) as the containing view's size changes.
- Provide a custom splitter.
- Be able to toggle layout between horizontal and vertical. This modifier is only available for the Split view, since HSplit and VSplit remain in a horizontal or vertical layout by definition.
In its simplest form, the HSplit and VSplit views look like this:
HSplit(left: { Color.red }, right: { Color.green })
VSplit(top: { Color.red }, bottom: { Color.green })
The HSplit is a horizontal split view, evenly split between red on the left and green on the right. The VSplit is a vertical split view, evenly split between red on the top and green on the bottom. Both views use a default splitter between them that can be dragged to change the red and green view sizes.
If you want to set the the initial position of the splitter, you can use the
fraction modifier. Here it is being used with a VSplit view:
VSplit(top: { Color.red }, bottom: { Color.green })
.fraction(0.25)
Now you get a red view above the green view, with the top occupying 1/4 of the window.
Often you want to hide/show one of the views you split. You can do this by specifying
the side to hide. Specify the side using a SplitSide. For an HSplit view, you can
identify the side using .left or .right. For a VSplit view, you can use .top
or .bottom. For a Split view (where the layout can change), use .primary or
.secondary. In fact, .left, .top, and .primary are all synonyms and can be
used interchangably. Similarly, .right, .bottom, and .secondary are synonyms.
Here is an HSplit view that hides the right side when it opens:
HSplit(left: { Color.red }, right: { Color.green })
.fraction(0.25)
.hide(.right)
The green side will be hidden, but you can pull it open using the splitter that will be visible on the right. This isn't usually what you want, though. Usually you want your users to be able to control whether a side is hidden or not. To do this, pass the SideHolder ObservableObject that holds onto the side you are hiding. Similarly the SplitView package comes with a FractionHolder and LayoutHolder. Under the covers, the Split view observes all of these holders and redraws itself if they change.
Here is an example showing how to use the SideHolder with a Button to hide/show the right (green) side:
struct ContentView: View {
let hide = SideHolder() // By default, don't hide any side
var body: some View {
VStack(spacing: 0) {
Button("Toggle Hide") {
withAnimation {
hide.toggle() // Toggle between hiding nothing and hiding right
}
}
HSplit(left: { Color.red }, right: { Color.green })
.hide(hide)
}
}
}
Note that the hide modifier accepts a SplitSide or a SideHolder. Similarly, layout
can be passed as a SplitLayout - .horizontal or .vertical - or as a LayoutHolder.
And fraction can be passed as a CGFloat or as a FractionHolder.
The toggle() method on hide toggles the hide/show state for the secondary side
by default. If you want to toggle the hide/show state for a specific side, then use
toggle(.primary) or toggle(.secondary) explicitly. (Note that .primary, .left,
and .top are synonyms; and .secondary, .right, and .bottom are synonyms.)
Nesting Split Views
Split views themselves can be split. Here is an example where the right side of an HSplit is a VSplit that has an HSplit at the bottom:
struct ContentView: View {
var body: some View {
HSplit(
left: { Color.green },
right: {
VSplit(
top: { Color.red },
bottom: {
HSplit(
left: { Color.blue },
right: { Color.yellow }
)
}
)
}
)
}
}
And here is one where an HSplit contains two VSplits:
struct ContentView: View {
var body: some View {
HSplit(
left: {
VSplit(top: { Color.red }, bottom: { Color.green })
},
right: {
VSplit(top: { Color.yellow }, bottom: { Color.blue })
}
)
}
}
Using UserDefaults For Split State
The three holders - SideHolder, LayoutHolder, and FractionHolder - all come with a
static method to return instances that get/set their state from UserDefaults.standard.
Let's expand the previous example to be able to change the layout and hide state
and to get/set their values from UserDefaults. Note that if you want to adjust the
layout, you need to use a Split view, not HSplit or VSplit. We create the Split view
by specifying the primary and secondary views. When the SplitLayout held by the
LayoutHolder (layout) is .horizontal, the primary view is on the left side, and
the secondary view is on the right. When the SplitLayout toggles to vertical, the
primary view is on the top, and the secondary view is on the bottom.
struct ContentView: View {
let fraction = FractionHolder.usingUserDefaults(0.5, key: "myFraction")
let layout = LayoutHolder.usingUserDefaults(.horizontal, key: "myLayout")
let hide = SideHolder.usingUserDefaults(key: "mySide")
var body: some View {
VStack(spacing: 0) {
HStack {
Button("Toggle Layout") {
withAnimation {
layout.toggle()
}
}
Button("Toggle Hide") {
withAnimation {
hide.toggle()
}
}
}
Split(primary: { Color.red }, secondary: { Color.green })
.fraction(fraction)
.layout(layout)
.hide(hide)
}
}
}
The first time you open this, the sides will be split 50-50, but as you drag the
splitter, the fraction state is also retained in UserDefaults.standard.
You can change the layout and hide/show the green view, and when you next open
the app, the fraction, hide, and layout will all be restored how you left them.
Modifying And Constraining The Default Splitter
You can change the way the default Splitter displays using the styling modifier.
For example, you can change the color, inset, and thickness:
HSplit(left: { Color.red }, right: { Color.green })
.fraction(0.25)
.styling(color: Color.cyan, inset: 4, visibleThickness: 8)
If you prefer the splitter to hide also when you hide a side, you can set hideSplitter
to true
Related Skills
node-connect
344.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
96.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
