Greetings, traveler!
While SwiftUI’s built-in Toggle control works perfectly for most cases, sometimes you want something more expressive—something that visually communicates state beyond just color or position. This small exploration shows how to attach a persistent image to a toggle and make it respond naturally to user interaction.
The Idea
The goal was simple: the toggle should always display an icon, positioned near the thumb, and change smoothly as the state changes or as the user interacts with it. Instead of rebuilding the toggle from scratch, the approach extends the standard Toggle with a small overlay and a drag gesture.
Reading Geometry
To position the icon correctly, the view needs to know the toggle’s width. Rather than hard-coding offsets, a lightweight GeometryReader observes layout changes:
.background(
GeometryReader { geo in
Color.clear
.onAppear {
viewWidth = geo.size.width
}
.onChange(of: geo.size.width) {
viewWidth = $0
}
}
)
Gesture Awareness
A DragGesture(minimumDistance: 0)
detects whether the touch begins on the left or right half of the control. This helps anticipate user intent—if they tap or drag on the right side, the icon immediately reflects that, providing tactile visual feedback even before the system toggle animation finishes.
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if !isPressing { isPressing = true }
let width = max(viewWidth, 1)
isRightSide = value.location.x > width / 2
}
.onEnded { _ in
isPressing = false
isRightSide = false
}
)
The Overlay Icon
The Image is rendered via overlay(alignment: .center)
and offset horizontally depending on both the toggle state and where the user is touching.
private func offset() -> CGFloat {
let half = max(viewWidth / 2 - 20, 12)
let goRight = (isPressing && isRightSide) || (isEnabled && !isPressing)
return (goRight ? half : -half)
}
This produces a subtle motion where the icon “sticks” to whichever side is active.
Selecting the Active Symbol
The helper function systemName()
determines which image should be displayed at any given moment. It evaluates both the persistent state (isEnabled
) and the transient interaction state (isPressing
and isRightSide
):
private func systemName() -> String {
let goRight = (isPressing && isRightSide) || (isEnabled && !isPressing)
return goRight ? systemNames.right : systemNames.left
}
Full code:
public struct LabeledToggle: View {
@Binding private var isEnabled: Bool
private let systemNames: (left: String, right: String)
@State private var isPressing = false
@State private var viewWidth: CGFloat = 0
@State private var isRightSide = false
public init(
isEnabled: Binding<Bool>,
systemNames: (left: String, right: String)
) {
_isEnabled = isEnabled
self.systemNames = systemNames
}
public var body: some View {
Toggle("", isOn: $isEnabled)
.labelsHidden()
.contentShape(.rect)
.background(
GeometryReader { geo in
Color.clear
.onAppear {
viewWidth = geo.size.width
}
.onChange(of: geo.size.width) {
viewWidth = $0
}
}
)
.overlay(alignment: .center) {
Image(systemName: systemName())
.offset(x: offset())
.allowsHitTesting(false)
}
.simultaneousGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if !isPressing { isPressing = true }
let width = max(viewWidth, 1)
isRightSide = value.location.x > width / 2
}
.onEnded { _ in
isPressing = false
isRightSide = false
}
)
}
private func offset() -> CGFloat {
let half = max(viewWidth / 2 - 20, 12)
let goRight = (isPressing && isRightSide) || (isEnabled && !isPressing)
return (goRight ? half : -half)
}
private func systemName() -> String {
let goRight = (isPressing && isRightSide) || (isEnabled && !isPressing)
return goRight ? systemNames.right : systemNames.left
}
}
Why Not a Custom ToggleStyle?
Creating a full custom ToggleStyle would provide more control over the thumb and track, but it also means re-implementing built-in system behaviors—sizes, tinting, animations. By extending Toggle instead, you inherit all of that for free while gaining enough flexibility to enrich its appearance.
Conclusion
This small component demonstrates how far a few SwiftUI primitives—geometry, gestures, and overlays—can go toward creating subtle, expressive UI behavior.
Available on GitHub.
If you enjoyed this article, please feel free to follow me on my social media: