One approach...
Play the "click" sound every time the "current tick mark" changes.
This will be slightly different, depending on how you are animating the "dial" -- but the concept is the same. Let's use a scroll view for example.
For the scrollable content, we'll use a view and draw a vertical "tick mark" every 20-points, taller on even 100-points positions. We'll also overlay a view with a single vertical line near the horizontal center - so we want to play a "click" when a tick hits that line. And we'll size things so we can only scroll horizontally.
It will look like this:

and after scrolling a little:

When implementing scrollViewDidScroll(...)
with a typical scroll view, it is very easy to scroll quickly... so quickly, that the .contentOffset.x
can change 200+ points between calls.
If we try to play the tick sound for every 20-points of change, we could be playing it 10 times at essentially the same time.
So, we could create a class property:
var prevTickMark: Int = 0
then calculate the current tick mark in scrollViewDidScroll(...)
. If the values are different, play a tick sound:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var cx = Int(scrollView.contentOffset.x)
// offset to the first tick-mark
cx += Int(scrollView.contentInset.left)
let curTick: Int = cx / 20
if prevTickMark != curTick {
// we just passed, or we are on, a new "tick"
// so play the tick sound
AudioServicesPlayAlertSound(SystemSoundID(1057))
prevTickMark = curTick
}
}
If we are scrolling / dragging very, very quickly, we don't need a click for every tick mark... because we are not seeing every tick mark cross the center-line.
As the scrolling decelerates -- or when dragging slowly -- we'll get a click on every tick.
Here's some quick example code to try out...
TickView - ticks every 20-points
class TickView: UIView {
lazy var tickLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
tickLayer.fillColor = nil
tickLayer.strokeColor = UIColor.red.cgColor
backgroundColor = .yellow
}
override func layoutSubviews() {
super.layoutSubviews()
let y: CGFloat = bounds.maxY * 0.75
let shortTick: CGFloat = bounds.maxY * 0.25
let tallTick: CGFloat = bounds.maxY * 0.5
let bez = UIBezierPath()
var pt: CGPoint = .init(x: bounds.minX, y: y)
// horizontal line full width of view
bez.move(to: pt)
bez.addLine(to: .init(x: bounds.maxX, y: pt.y))
// add vertical "tick" lines every 20-points
// with a taller line every 100-points
bez.move(to: pt)
while pt.x <= bounds.maxX {
bez.move(to: pt)
if Int(pt.x) % 100 == 0 {
bez.addLine(to: .init(x: pt.x, y: pt.y - tallTick))
} else {
bez.addLine(to: .init(x: pt.x, y: pt.y - shortTick))
}
pt.x += 20.0
}
tickLayer.path = bez.cgPath
}
}
MidLineView - vertical line to overlay on the scroll view
class MidLineView: UIView {
lazy var midLineLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
midLineLayer.fillColor = nil
midLineLayer.strokeColor = UIColor.blue.cgColor
backgroundColor = .clear
}
override func layoutSubviews() {
super.layoutSubviews()
let bez = UIBezierPath()
// we want the mid line to be *about* at the horizontal center
// but at an even 20-points
var x: Int = Int(bounds.midX)
x -= x % 20
bez.move(to: .init(x: CGFloat(x), y: bounds.minY))
bez.addLine(to: .init(x: CGFloat(x), y: bounds.maxY))
midLineLayer.path = bez.cgPath
}
}
ViewController - example controller
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
// view with "tick-mark" lines every 20-points
let tickView = TickView()
// view with single vertical line
// overlay on the scroll view so we have a
// "center-line"
let midLineView = MidLineView()
// track the previous "tick"
var prevTickMark: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
tickView.translatesAutoresizingMaskIntoConstraints = false
midLineView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(tickView)
view.addSubview(scrollView)
view.addSubview(midLineView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.heightAnchor.constraint(equalToConstant: 120.0),
scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tickView.topAnchor.constraint(equalTo: cg.topAnchor),
tickView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
tickView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
tickView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
// let's make the "tick" view 2000-points wide
// so we have a good amount of scrolling distance
tickView.widthAnchor.constraint(equalToConstant: 2000.0),
tickView.heightAnchor.constraint(equalTo: fg.heightAnchor, multiplier: 1.0),
midLineView.topAnchor.constraint(equalTo: scrollView.topAnchor),
midLineView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
midLineView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
midLineView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
])
scrollView.delegate = self
// disable interaction on the overlaid view
midLineView.isUserInteractionEnabled = false
// so we can see the framing of the scroll view
scrollView.backgroundColor = .lightGray
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// offsets so the "ticks" start and end near the horiztonal center
// on even 20-points
var x: Int = Int(scrollView.frame.width * 0.5)
x -= x % 20
scrollView.contentInset = .init(top: 0.0, left: CGFloat(x), bottom: 0.0, right: scrollView.frame.width - CGFloat(x))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var cx: Int = Int(scrollView.contentOffset.x)
// offset to the first tick-mark
cx += Int(scrollView.contentInset.left)
let curTick: Int = cx / 20
if prevTickMark != curTick {
// we just passed, or we are on, a new "tick"
// so play the tick sound
AudioServicesPlayAlertSound(SystemSoundID(1057))
prevTickMark = curTick
}
}
}