Skip to content

Instantly share code, notes, and snippets.

@kylehughes
Last active May 17, 2022 15:46
Show Gist options
  • Save kylehughes/e9f7851c9f8d37d348f514f199528adb to your computer and use it in GitHub Desktop.
Save kylehughes/e9f7851c9f8d37d348f514f199528adb to your computer and use it in GitHub Desktop.
Convenient Swift abstractions for generating haptic feedback on iOS.
// Copyright 2021 Kyle Hughes
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#if canImport(UIKit)
import Foundation
import UIKit
// MARK: - HapticFeedback Definition
/// A convenience wrapper for `HapticFeedbackGenerator.shared`.
public enum HapticFeedback {
// MARK: Public Static Interface
@inlinable
public static func generate(using preparedFeedback: HapticFeedbackGenerator.PreparedFeedback) {
HapticFeedbackGenerator.shared.generate(using: preparedFeedback)
}
@inlinable
public static func generate(_ feedback: HapticFeedbackGenerator.Feedback?) {
HapticFeedbackGenerator.shared.generate(feedback)
}
@inlinable
public static func generate(_ feedback: HapticFeedbackGenerator.Feedback) {
HapticFeedbackGenerator.shared.generate(feedback)
}
@inlinable
public static func generate(for semanticFeedback: HapticFeedbackGenerator.SemanticFeedback?) {
HapticFeedbackGenerator.shared.generate(for: semanticFeedback)
}
@inlinable
public static func generate(for semanticFeedback: HapticFeedbackGenerator.SemanticFeedback) {
HapticFeedbackGenerator.shared.generate(for: semanticFeedback)
}
@inlinable
public static func prepare(
_ feedback: HapticFeedbackGenerator.Feedback
) -> HapticFeedbackGenerator.PreparedFeedback {
HapticFeedbackGenerator.shared.prepare(feedback)
}
@inlinable
public static func prepare(
for semanticFeedback: HapticFeedbackGenerator.SemanticFeedback
) -> HapticFeedbackGenerator.PreparedFeedback {
HapticFeedbackGenerator.shared.prepare(for: semanticFeedback)
}
@inlinable
public static func prepareAgain(_ preparedFeedback: HapticFeedbackGenerator.PreparedFeedback) {
HapticFeedbackGenerator.shared.prepareAgain(preparedFeedback)
}
@inlinable
public static func setIsDisabled(_ isDisabled: Bool) {
HapticFeedbackGenerator.shared.setIsDisabled(isDisabled)
}
@inlinable
public static func setIsDisabled(basedOn isDisabledKey: String, in userDefaults: UserDefaults = .standard) {
HapticFeedbackGenerator.shared.setIsDisabled(basedOn: isDisabledKey, in: userDefaults)
}
@inlinable
public static func setIsDisabled(basedOn isDisabledProvider: @escaping HapticFeedbackGenerator.IsDisabledProvider) {
HapticFeedbackGenerator.shared.setIsDisabled(basedOn: isDisabledProvider)
}
@inlinable
public static func setIsEnabled(_ isEnabled: Bool) {
HapticFeedbackGenerator.shared.setIsEnabled(isEnabled)
}
@inlinable
public static func setIsEnabled(basedOn isEnabledKey: String, in userDefaults: UserDefaults = .standard) {
HapticFeedbackGenerator.shared.setIsEnabled(basedOn: isEnabledKey, in: userDefaults)
}
@inlinable
public static func setIsEnabled(basedOn isEnabledProvider: @escaping HapticFeedbackGenerator.IsEnabledProvider) {
HapticFeedbackGenerator.shared.setIsEnabled(basedOn: isEnabledProvider)
}
}
// MARK: - HapticFeedbackGenerator Definition
public final class HapticFeedbackGenerator {
public typealias IsDisabledProvider = () -> Bool
public typealias IsEnabledProvider = () -> Bool
public static let shared = HapticFeedbackGenerator()
private var isEnabledProvider: IsEnabledProvider
// MARK: Public Initialization
public convenience init(isDisabled: Bool) {
self.init(
isDisabledProvider: {
isDisabled
}
)
}
public convenience init(isDisabledKey: String, userDefaults: UserDefaults = .standard) {
self.init(
isDisabledProvider: {
userDefaults.bool(forKey: isDisabledKey)
}
)
}
public convenience init(isDisabledProvider: @escaping IsDisabledProvider) {
self.init(
isEnabledProvider: {
!isDisabledProvider()
}
)
}
public convenience init(isEnabled: Bool) {
self.init(
isEnabledProvider: {
isEnabled
}
)
}
public convenience init(isEnabledKey: String, userDefaults: UserDefaults = .standard) {
self.init(
isEnabledProvider: {
userDefaults.bool(forKey: isEnabledKey)
}
)
}
public convenience init() {
self.init(isEnabled: true)
}
public init(isEnabledProvider: @escaping IsEnabledProvider) {
self.isEnabledProvider = isEnabledProvider
}
// MARK: Public Instance Interface
public func generate(using preparedFeedback: PreparedFeedback) {
preparedFeedback()
}
public func generate(_ feedback: Feedback?) {
guard let feedback = feedback else {
return
}
generate(feedback)
}
public func generate(_ feedback: Feedback) {
FeedbackAndGenerator.from(feedback, isEnabledProvider)()
}
public func generate(for semanticFeedback: SemanticFeedback?) {
guard let semanticFeedback = semanticFeedback else {
return
}
generate(semanticFeedback.base)
}
public func generate(for semanticFeedback: SemanticFeedback) {
generate(semanticFeedback.base)
}
public func prepare(_ feedback: Feedback) -> PreparedFeedback {
PreparedFeedback(using: .from(feedback, isEnabledProvider))
}
public func prepare(for semanticFeedback: SemanticFeedback) -> PreparedFeedback {
prepare(semanticFeedback.base)
}
public func prepareAgain(_ preparedFeedback: PreparedFeedback) {
preparedFeedback.prepareAgain()
}
public func setIsDisabled(_ isDisabled: Bool) {
setIsDisabled {
isDisabled
}
}
public func setIsDisabled(basedOn isDisabledKey: String, in userDefaults: UserDefaults = .standard) {
setIsDisabled {
userDefaults.bool(forKey: isDisabledKey)
}
}
public func setIsDisabled(basedOn isDisabledProvider: @escaping IsDisabledProvider) {
setIsEnabled {
!isDisabledProvider()
}
}
public func setIsEnabled(_ isEnabled: Bool) {
setIsEnabled {
isEnabled
}
}
public func setIsEnabled(basedOn isEnabledKey: String, in userDefaults: UserDefaults = .standard) {
setIsEnabled {
userDefaults.bool(forKey: isEnabledKey)
}
}
public func setIsEnabled(basedOn isEnabledProvider: @escaping IsEnabledProvider) {
self.isEnabledProvider = isEnabledProvider
}
}
// MARK: - HapticFeedbackGenerator.Feedback Definition
extension HapticFeedbackGenerator {
public enum Feedback {
case impact(Impact)
case notification(Notification)
case selection(Selection)
// MARK: Public Static Interface
public static var selection: Feedback {
.selection(.selectionChanged)
}
}
}
// MARK: - HapticFeedbackGenerator.Feedback.Impact Definition
extension HapticFeedbackGenerator.Feedback {
/// - Attention: Intensity, if supplied, expects a value in the range [0.0, 1.0].
public enum Impact {
case heavy(CGFloat? = nil)
case light(CGFloat? = nil)
case medium(CGFloat? = nil)
case rigid(CGFloat? = nil)
case soft(CGFloat? = nil)
// MARK: Public Static Interface
public static var heavy: Impact {
.heavy()
}
public static var light: Impact {
.light()
}
public static var medium: Impact {
.medium()
}
public static var rigid: Impact {
.rigid()
}
public static var soft: Impact {
.soft()
}
// MARK: Public Instance Interface
public var intensity: CGFloat? {
switch self {
case
let .heavy(intensity),
let .light(intensity),
let .medium(intensity),
let .rigid(intensity),
let .soft(intensity)
:
return intensity
}
}
public var platformType: UIImpactFeedbackGenerator.FeedbackStyle {
switch self {
case .heavy:
return .heavy
case .light:
return .light
case .medium:
return .medium
case .rigid:
return .rigid
case .soft:
return .soft
}
}
}
}
// MARK: - HapticFeedbackGenerator.Feedback.Notification Definition
extension HapticFeedbackGenerator.Feedback {
public enum Notification {
case error
case success
case warning
// MARK: Public Instance Interface
public var platformType: UINotificationFeedbackGenerator.FeedbackType {
switch self {
case .error:
return .error
case .success:
return .success
case .warning:
return .warning
}
}
}
}
// MARK: - HapticFeedbackGenerator.Feedback.Selection Definition
extension HapticFeedbackGenerator.Feedback {
public enum Selection {
case selectionChanged
}
}
// MARK: - HapticFeedbackGenerator.FeedbackAndGenerator Definition
extension HapticFeedbackGenerator {
fileprivate enum FeedbackAndGenerator {
case impact(Feedback.Impact, UIImpactFeedbackGenerator, IsEnabledProvider)
case notification(Feedback.Notification, UINotificationFeedbackGenerator, IsEnabledProvider)
case selection(Feedback.Selection, UISelectionFeedbackGenerator, IsEnabledProvider)
// MARK: Fileprivate Static Interface
fileprivate static func from(
_ feedback: Feedback,
_ isEnabledProvider: @escaping IsEnabledProvider
) -> FeedbackAndGenerator {
switch feedback {
case let .impact(feedback):
return .impact(feedback, isEnabledProvider)
case let .notification(feedback):
return .notification(feedback, isEnabledProvider)
case let .selection(feedback):
return .selection(feedback, isEnabledProvider)
}
}
fileprivate static func impact(
_ feedback: Feedback.Impact,
_ isEnabledProvider: @escaping IsEnabledProvider
) -> FeedbackAndGenerator {
.impact(feedback, UIImpactFeedbackGenerator(style: feedback.platformType), isEnabledProvider)
}
fileprivate static func notification(
_ feedback: Feedback.Notification,
_ isEnabledProvider: @escaping IsEnabledProvider
) -> FeedbackAndGenerator {
.notification(feedback, UINotificationFeedbackGenerator(), isEnabledProvider)
}
fileprivate static func selection(
_ feedback: Feedback.Selection,
_ isEnabledProvider: @escaping IsEnabledProvider
) -> FeedbackAndGenerator {
.selection(feedback, UISelectionFeedbackGenerator(), isEnabledProvider)
}
// MARK: Fileprivate Instance Interface
fileprivate func callAsFunction() {
switch self {
case let .impact(impact, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
if let intensity = impact.intensity {
generator.impactOccurred(intensity: intensity)
} else {
generator.impactOccurred()
}
case let .notification(notification, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
generator.notificationOccurred(notification.platformType)
case let .selection(selection, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
switch selection {
case .selectionChanged:
generator.selectionChanged()
}
}
}
fileprivate func prepare() {
switch self {
case let .impact(_, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
generator.prepare()
case let .notification(_, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
generator.prepare()
case let .selection(_, generator, isEnabledProvider):
guard isEnabledProvider() else {
return
}
generator.prepare()
}
}
}
}
// MARK: - HapticFeedbackGenerator.PreparedFeedback Definition
extension HapticFeedbackGenerator {
public struct PreparedFeedback {
fileprivate let feedbackAndGenerator: FeedbackAndGenerator
// MARK: Fileprivate Initialization
fileprivate init(using feedbackAndGenerator: FeedbackAndGenerator) {
self.feedbackAndGenerator = feedbackAndGenerator
feedbackAndGenerator.prepare()
}
// MARK: Public Instance Interface
public func callAsFunction() {
feedbackAndGenerator()
}
public func prepareAgain() {
feedbackAndGenerator.prepare()
}
}
}
// MARK: - HapticFeedbackGenerator.SemanticFeedback Definition
extension HapticFeedbackGenerator {
public struct SemanticFeedback {
fileprivate let base: Feedback
// MARK: Public Initialization
public init(_ base: Feedback) {
self.base = base
}
}
}
#endif
@kylehughes
Copy link
Author

kylehughes commented Aug 8, 2021

Haptic Feedback Generator

Convenient Swift abstractions for generating haptic feedback on iOS.

Basic Usage

Generating Feedback

All feedback types are accessible via enum cases.

e.g.

HapticFeedback.generate(.notification(.success))

or

HapticFeedback.generate(.impact(.heavy(.5))

or

HapticFeedback.generate(.selection)

Preparing Feedback

e.g.

let preparedFeedback = HapticFeedback.prepare(.impact(.success))



preparedFeedback()



preparedFeedback.prepareAgain()



preparedFeedback()

Enabling & Disabling Haptics

Haptics are enabled by default.

Based on UserDefaults

Replace with your own UserDefaults key.

e.g.

HapticFeedback.setIsDisabled(basedOn: "ReduceHaptics")

or

HapticFeedback.setIsEnabled(basedOn: "AreHapticsEnabled")

Based on Constant Value

e.g.

HapticFeedback.setIsDisabled(true)

or

HapticFeedback.setIsEnabled(true)

Based on Closure

e.g.

HapticFeedback.setIsDisabled {
    // Return boolean
}

or

HapticFeedback.setIsEnabled {
    // Return boolean
}

Semantic Usage

The HapticFeedbackGenerator.SemanticFeedback type is provided as a convenient namespace to put your own constants to ensure consistency throughout your application and focus the scope of code completion. The functions that accept SemanticFeedback are written to optimize for fluent call sites.

Using this type is optional. Constants may also be extended onto the HapticFeedbackGenerator.Feedback type but the call sites will be less fluent and code completion will include the base cases.

e.g.

extension HapticFeedbackGenerator.SemanticFeedback {
    static let sheetPresentation = Self(.impact(.medium))
}



HapticFeedback.generate(for: .sheetPresentation)

License

MIT

@kylehughes
Copy link
Author

kylehughes commented Aug 8, 2021

Example swift

Created at https://ray.so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment