A SwiftUI scroll-wheel dial control for iOS and macOS.
Originally created as an Objective-C component in 2012. Rewritten in SwiftUI in 2025.
- 9 built-in visual styles + custom image tile support
- Adjustable dial width (80–220 pt)
- Optional stepper buttons with decimal label
- Velocity-sensitive drag with inertia (flick to coast)
- Step-snapping drag — no visual jump on finger lift
- Navigation swipe-back blocking during dial interaction (iOS)
- Haptic feedback on every step (iOS)
AZDialInteractionTuning— fully tunable interaction parametersAZDialSettingsView— ready-made settings sheet for style and sensitivity- VoiceOver / Accessibility support
- Dark mode support
- Pure SwiftUI — no UIKit wrappers required in your code
- iOS 16.0+ / macOS 13.0+
- Swift 5.9+
- Xcode 15+
In Xcode: File → Add Package Dependencies
https://github.com/SumPositive/AZDial
Or add to your Package.swift:
dependencies: [
.package(url: "https://github.com/SumPositive/AZDial", from: "3.0.0")
]import AZDial
struct ContentView: View {
@State private var weight = 600 // 60.0 kg (stored ×10)
var body: some View {
AZDialView(
value: $weight,
min: 300, // 30.0 kg
max: 2000, // 200.0 kg
step: 1,
stepperStep: 10,
decimals: 1,
style: .shape,
dialWidth: 220
)
}
}| Parameter | Type | Default | Description |
|---|---|---|---|
value |
Binding<Int> |
— | Current value |
min |
Int |
— | Minimum value |
max |
Int |
— | Maximum value |
step |
Int |
— | Value increment per drag step |
stepperStep |
Int |
— | Stepper button increment (0 = hidden) |
decimals |
Int |
0 |
Decimal places shown on stepper label |
style |
DialStyle |
.shape |
Visual style |
dialWidth |
CGFloat |
220 |
Dial width in points (clamped to 80–220) |
tuning |
AZDialInteractionTuning? |
nil (= .default) |
Interaction tuning; overrides pitch when set |
pitch |
CGFloat |
20 |
Shorthand for tuning.pitch when tuning is nil |
| Style | Description |
|---|---|
.regacy |
Classic AZDial knurling — original Objective-C design |
.midnight |
Dark gunmetal with high-contrast silver highlights |
.brass |
Warm brass with champagne gold highlights |
.ocean |
Blue anodized aluminum with ice-blue highlights |
.shape |
Shape-based knurling tile (default) |
.varnia |
Narrow machined knurling |
.chrome |
Polished chrome with high contrast |
.hairline |
Ultra-fine hairline engraving |
.rubber |
Wide matte rubber grip |
AZDialView(value: $value, min: 0, max: 100, step: 1, stepperStep: 10,
style: .shape)Supply your own PDF or PNG image from Assets.xcassets:
AZDialView(
value: $value,
min: 0, max: 100,
step: 1, stepperStep: 10,
style: .tile(
light: "MyDialTile",
dark: "MyDialTile_Dark", // optional
tileWidth: 20 // must match image width in points
)
)For assets in your own Swift Package, pass bundle: .module:
style: .tile(light: "MyDialTile", tileWidth: 20, bundle: .module)Use DialStyle.id (a String) to store the selected style in UserDefaults or iCloud KVS, and restore it with DialStyle.builtin(id:):
// Save
UserDefaults.standard.set(style.id, forKey: "dialStyle")
// Restore
let id = UserDefaults.standard.string(forKey: "dialStyle") ?? ""
let style = DialStyle.builtin(id: id) ?? .shapeAZDialInteractionTuning controls all drag and inertia parameters. Pass it via the tuning argument of AZDialView.
@State private var tuning = AZDialInteractionTuning.default
AZDialView(value: $value, min: 0, max: 100, step: 1, stepperStep: 10,
tuning: tuning)| Property | Type | Default | Description |
|---|---|---|---|
pitch |
CGFloat |
20 |
Points of horizontal drag per value step |
velocitySmoothing |
CGFloat |
0.4 |
Weight of the newest velocity sample (0–1) |
inertiaStartVelocity |
CGFloat |
200 |
Minimum velocity (pt/s) to start inertia after lift |
fastSwipeVelocity |
CGFloat |
1500 |
Velocity boundary between slow and fast multiplier |
slowSwipeMultiplier |
Int |
10 |
Steps per pitch during slow inertia |
fastSwipeMultiplier |
Int |
100 |
Steps per pitch during fast inertia |
inertiaDecay |
CGFloat |
0.94 |
Per-frame velocity multiplier during coast (0.80–0.99) |
inertiaStopVelocity |
CGFloat |
15 |
Velocity at which inertia stops |
| Preset | pitch |
Multipliers | Feel |
|---|---|---|---|
.fine |
36 pt | ×3 / ×20 | Maximum precision, minimal inertia |
.mild |
28 pt | ×6 / ×50 | Controlled |
.standard |
20 pt | ×10 / ×100 | Default |
.light |
14 pt | ×15 / ×130 | Nimble |
.fast |
9 pt | ×20 / ×180 | High-speed entry |
tuning = AZDialInteractionTuningPreset.fine.tuningA ready-made settings sheet for choosing a dial style and interaction sensitivity. Present it from your app's settings screen and persist the bindings however you prefer.
@State private var style = DialStyle.shape
@State private var tuning = AZDialInteractionTuning.default
@State private var showSettings = false
Button("Dial Settings") { showSettings = true }
.sheet(isPresented: $showSettings) {
AZDialSettingsView(tuning: $tuning, style: $style)
}The sheet exposes a live-preview dial, preset buttons, and fine-grained sliders.
Customize it with AZDialSettingsConfiguration:
let config = AZDialSettingsConfiguration(
title: "Input Wheel",
styleCandidates: [.shape, .chrome, .rubber],
testRange: 0...999
)
AZDialSettingsView(tuning: $tuning, style: $style, configuration: config)AZDialInteractionTuning is Codable, so you can round-trip it through UserDefaults or iCloud KVS:
// Save
if let data = try? JSONEncoder().encode(tuning) {
UserDefaults.standard.set(data, forKey: "dialTuning")
}
// Restore
if let data = UserDefaults.standard.data(forKey: "dialTuning"),
let saved = try? JSONDecoder().decode(AZDialInteractionTuning.self, from: data) {
tuning = saved
}Use AZDialSurface if you need only the scrolling ridge background — for example, in a settings UI preview:
AZDialSurface(offset: 0, tickGap: 10, style: .brass)
.frame(height: 44)
.clipShape(RoundedRectangle(cornerRadius: 8))Open AZDial.xcworkspace (not the package directly) to work on both the library and the demo app together.
AZDial/
├── AZDial.xcworkspace ← open this
├── Package.swift
├── Sources/AZDial/
│ ├── AZDialView.swift
│ └── Resources/Assets.xcassets/
├── Tests/AZDialTests/
└── Examples/AZDialDemo/
🎨 Cool design proposals welcome!
If you create an original tile image for AZDial, please share it via Issues or Pull Requests. Outstanding designs will be showcased in the demo app and may be adopted as official built-in styles.
MIT License. See LICENSE for details.


