Greetings, tarveler!
When you need to visually compare two images, screens, or states of a view, a “before and after” slider is often the most intuitive solution. In this article, we’ll walk through how to build a reusable comparison slider view entirely in SwiftUI — without relying on UIKit or external libraries.
Concept Overview
The main idea is to overlay two views on top of each other and use a mask to reveal only part of the upper layer. A draggable divider defines how much of the upper view remains visible.
This kind of component is often used for:
- Comparing edited vs. original photos
- Demonstrating UI redesigns
- Visualizing changes between states (light/dark themes, animations, etc.)
The View Structure
We’ll define a generic container SliderComparisonView<Left: View, Right: View>
that accepts two SwiftUI views — one for each side.
public var body: some View {
GeometryReader { geometry in
ZStack {
Color.clear
.overlay {
lhs()
}
Color.clear
.overlay {
rhs()
}
.mask {
Rectangle()
.offset(x: dividerLocation + geometry.size.width / 2)
}
dividerView()
.offset(x: dividerLocation)
}
.gesture(
DragGesture()
.onChanged { gesture in
dividerLocation = min(
max(gesture.location.x - geometry.size.width / 2, -geometry.size.width / 2),
geometry.size.width / 2
)
}
)
}
.ignoresSafeArea()
}
The core idea is simple:
- Stack both views in a
ZStack
. - Mask the upper (right-hand) layer with a rectangle.
- Adjust the mask and the divider’s position based on the drag gesture.
Masking the Right View
The mask controls which portion of the upper layer is visible.
Color.clear
.overlay {
rhs()
}
.mask {
Rectangle()
.offset(x: dividerLocation + geometry.size.width / 2)
}
Here’s what happens:
- The
Rectangle()
acts as a “window” through which the upper view is visible. - Moving the mask horizontally (
offset(x:)
) hides or reveals parts of the right view. - The offset is computed relative to the center of the container, so
dividerLocation
can be positive or negative.
Building the Divider
The divider itself is both visual and interactive. It indicates where the split occurs and provides a handle for dragging.
private func dividerView() -> some View {
Rectangle()
.fill(dividerColor)
.frame(width: dividerWidth)
.overlay {
Circle()
.fill(indicatorColor)
.frame(width: indicatorWidth)
.overlay {
indicatorImage
.resizable()
.scaledToFit()
.frame(width: indicatorImageWidth)
.foregroundColor(indicatorImageColor)
}
}
}
This small circle with a directional arrow gives users a clear affordance to interact with.
Gesture Handling
The gesture logic ensures smooth horizontal dragging within the view’s bounds:
.onChanged { gesture in
dividerLocation = min(
max(gesture.location.x - geometry.size.width / 2, -geometry.size.width / 2),
geometry.size.width / 2
)
}
This clamps the divider position to the view’s width, preventing it from moving beyond the visible area.
Example Usage
You can use the comparison view with any SwiftUI content — not just images:
SliderComparisonView {
Image(.winter)
} rhs: {
Image(.spring)
}
SliderComparisonView(
lhs: { Text("Some Text").font(.title).fontWeight(.heavy).foregroundColor(.gray) },
rhs: { Text("Some Text").font(.title).fontWeight(.heavy).foregroundColor(.blue) }
)
Final Thoughts
By encapsulating the logic inside a reusable SliderComparisonView
, you can drop this component into any SwiftUI project — whether for visual comparison, transition demos, or creative UI interactions.