// // NBHaptic.swift // NimbleKit // // One call site for tactile feedback, so the whole app taps the same way. // #if os(iOS) import UIKit /// Tactile feedback, in the three kinds iOS actually distinguishes. /// /// Picking between them is not decoration. `tap` is "you pressed a thing", /// `selection` is "the thing you were scrubbing moved a notch", and `notify` is /// "it worked" or "it didn't". Using an impact where a notification belongs /// throws away the only part a person can feel the difference in. /// /// Deliberately stateless: no cached generators. A generator held as static /// mutable state is shared concurrency-checked state for the sake of a /// `prepare()` whose lead time is measured in the milliseconds between creating /// it and firing it, which is to say none. Cheap to make, so make one. public enum NBHaptic { /// A press. The default weight is for ordinary primary actions (install, /// update, open); reach for a heavier style only when the action is heavier. public static func tap(_ style: UIImpactFeedbackGenerator.FeedbackStyle = .light) { _onMain { let generator = UIImpactFeedbackGenerator(style: style) generator.prepare() generator.impactOccurred() } } /// An outcome: success, warning, failure. Never for a button press. public static func notify(_ type: UINotificationFeedbackGenerator.FeedbackType) { _onMain { let generator = UINotificationFeedbackGenerator() generator.prepare() generator.notificationOccurred(type) } } /// A discrete step: a picker moving on, a segment changing, a control /// snapping to a position. public static func selection() { _onMain { let generator = UISelectionFeedbackGenerator() generator.prepare() generator.selectionChanged() } } /// `UIFeedbackGenerator` is main-thread only, and plenty of the places worth /// giving feedback for are completion handlers on somebody else's queue. /// Hopping here rather than at each call site means no caller has to know. /// /// `@Sendable` because this package builds under a newer language mode than /// the app does, where `DispatchQueue.async(execute:)` requires it. Every /// closure passed in captures nothing but a feedback style, so it costs /// nothing to promise. private static func _onMain(_ body: @escaping @Sendable () -> Void) { if Thread.isMainThread { body() } else { DispatchQueue.main.async(execute: body) } } } #endif