A tactile, momentum-based radial control for SwiftUI β inspired by analog flywheels.
![]()
A SwiftUI-based, physics-inspired radial scroller for zooming, scrubbing, and precision value adjustments.
FlywheelControl mimics the feel of a real-world dial β complete with momentum, resistance, haptic feedback, and pen-friendly input.
We needed a more natural way to zoom β something better than pinch and expand.
So we built a rotary-style control that works with a finger or stylus, and feels real thanks to physics and CoreHaptics.
- ποΈ Inertial spinning like a physical dial
- π± One-finger- and Apple Pencil-friendly
- π₯ Haptic ticks for tactile feedback
- π¨ Fully SwiftUI and easy to customize
- π§ Binding-driven: tracks a zoom
positionwith clamped min/max offsets and live span adjustments - π Smooth momentum decay and natural stopping behavior
- π¨ Custom skin support: Apply your own ruler artwork for a fully branded experience
- π Clamp limits: minValue and maxValue must be β₯ 0
- π spanCM defines visible range (must be > 0)
- π Optional clamping: turn off min/max limits for free spinning in auto modes
In Xcode:
- Go to
File β Add Packagesβ¦ - Enter the URL:
https://github.com/aweiner42/FlywheelControl - Choose the latest version (e.g.,
1.2.7)
Or add it to your Package.swift:
.package(url: "https://github.com/aweiner42/FlywheelControl.git", from: "1.2.7")Then add the dependency to your target:
.target(
name: "YourApp",
dependencies: ["FlywheelControl"]
)Import the module where needed:
import FlywheelControlPlace your ruler artwork (e.g., RulerSkin.png) in your appβs Asset Catalog (Assets.xcassets) and assign it the name RulerSkin. FlywheelControl will automatically load this image if present.
The package includes a sample ruler skin (DefaultRuler.png). You can copy it into your appβs Asset Catalog and rename it RulerSkin for immediate use.
When using Swift Playgrounds, FlywheelControl automatically falls back to its bundled reference skin unless a custom track image is explicitly provided.
FlywheelControl can be explored interactively on an iPad using Swift Playgrounds β no Xcode required. This is intended as a lightweight DX front door so engineers can feel inertia and tuning before integrating the SDK into an app. The Playground configuration intentionally mirrors the demo app defaults to avoid misconfiguration during first exploration.
- Swift Playgrounds on iPad
- FlywheelControl v1.2.7+ (includes Swift Playgrounds gesture fix)
- iPad hardware (momentum/inertia works great; haptics are best on iPhone)
- Open Swift Playgrounds and create a new App (SwiftUI template).
- Add the package:
- Tap + β Swift Package
- Paste:
https://github.com/aweiner42/FlywheelControl
- Replace ContentView.swift with the minimal demo below (note the non-zero ranges) and run.
import SwiftUI
import FlywheelControl
struct ContentView: View {
@State private var value: Double = 0 // current value in cm
@State private var maxValue: Double = 150
@State private var minValue: Double = 0
@State private var spanCM: Double = 20 // visible range in cm
var body: some View {
VStack(spacing: 40) {
Text("Value: \(value, specifier: "%.2f") cm")
.font(.headline)
FlywheelControl(
trackImage: FlywheelControlResources.bundledRulerImage(),
position: $value,
maxOffset: $maxValue,
minOffset: $minValue,
spanCM: $spanCM
)
.frame(width: 60, height: 240)
}
.padding()
}
}When no custom image is supplied, FlywheelControl uses its bundled reference ruler via FlywheelControlResources.
This configuration mirrors the bundled demo app and is recommended for first-time exploration.
Use smaller spanCM values (e.g. 20) for a tactile feel. Drag interaction works in Swift Playgrounds on iPad and macOS starting in v1.2.7.
The demo ruler artwork is bundled with the FlywheelControl package and loaded via a public resource accessor.
When exploring the control, ensure minOffset, maxOffset, and spanCM are all greater than zero so the dial can move freely.
@State private var zoomPosition: Double = 0
@State private var minZoom: Double = 1
@State private var maxZoom: Double = 90
@State private var zoomSpan: Double = 20
var body: some View {
FlywheelControl(
trackImage: Image("RulerSkin"), // custom ruler skin from Assets
position: $zoomPosition, // current scroll/zoom value
maxOffset: $maxZoom, // maximum scroll value (must be > 0)
minOffset: $minZoom, // minimum scroll value (must be > 0)
spanCM: $zoomSpan, // visible range in cm (must be > 0)
disableClampLimits: true
)
.onChange(of: zoomPosition) { newValue in
zoomManager.adjustZoom(by: CGFloat(newValue))
}
}- iOS 17.0+
- macOS 12.0+
- Swift 5.9+
- SwiftUI + Combine
FlywheelControl includes:
- π SwiftUI Previews
- π§± Modular design (no app dependencies)
Clone this repo and open FlywheelDemoApp/FlywheelDemoApp.xcodeproj to explore the latest refinements including improved momentum physics and control skinning.
Alan Weiner β’ SIME Corp
Inventor. Designer. Engineer. Collaborating with AI to shape intuitive interfaces.
Version 1.2.7 includes improved Swift Playgrounds drag reliability, refined gesture handling, and support for custom ruler skins.