Greetings, traveler!
When it comes to great UX in mobile apps, even small details can create moments of delight. In this post, I’ll show you how to build a unique, tactile UI element in SwiftUI — a pull-cord switch, inspired by the kind used to turn lights on and off. It’s a fun and physical-feeling way for users to switch your app’s theme.
What Are We Building?
Instead of a standard toggle, we’ll create a vertical rope hanging from the top of the screen. When the user pulls it down far enough, a callback is triggered — like switching a light on. The interaction feels physical and intuitive, adding personality to your app.
The view includes:
- A long, thin rope (a vertical rectangle),
- A downward arrow icon at the bottom,
- Bouncy animation for a realistic effect,
- Drag gesture detection with a custom threshold.
Check out this code:
struct PullCordView: View {
let onPulled: () -> Void
@State private var dragOffset: CGFloat = .zero
private let maxPullDistance: CGFloat = 100
private let maxAngle: Double = 10
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.primary)
.frame(width: 3, height: 500)
Image(systemName: "arrow.down.circle.fill")
.resizable()
.scaledToFill()
.frame(width: 22, height: 22)
.padding(.top, -2)
.foregroundStyle(.primary)
}
.animation(.bouncy(extraBounce: 0.28), value: dragOffset)
.frame(width: 29)
.contentShape(Rectangle())
.compositingGroup()
.gesture(
DragGesture(minimumDistance: 10)
.onChanged { value in
let delta = value.translation.height
guard delta > value.translation.width else { return }
if delta > 0 {
dragOffset = min(delta, maxPullDistance)
}
}
.onEnded { value in
guard value.translation.height > value.translation.width else { return }
if dragOffset > 60 {
onPulled()
}
dragOffset = 0
}
)
.offset(y: dragOffset)
.padding(.top, -200)
}
}
How It Works
dragOffset
tracks how far the rope has been pulled.- The
DragGesture
listens only for vertical pulls, ignoring horizontal ones. - If the pull distance exceeds a threshold (60 points), the
onPulled
callback is fired. - The
.bouncy
animation adds a satisfying elastic feel when the rope is released.
Now, you can use it like this:
struct SettingsView: View {
private let themeManager = ThemeManager()
var body: some View {
NavigationStack {
List {
themeSwitcherView
}
.overlay(alignment: .topTrailing) {
PullCordView {
switchAppearance()
}
.ignoresSafeArea()
}
.onChange(of: selectedAppearance) { _, _ in
themeManager.overrideDisplayMode()
}
}
}
}
What about theme switching?
As you can see, I am using a ThemeManager here. You can read more about it here.
Final Thoughts
With just a bit of geometry, gesture recognition, and animation, you can give users a micro-experience that stands out.
If you enjoyed this article, please feel free to follow me on my social media: