Last active
May 17, 2022 15:46
-
-
Save kylehughes/e9f7851c9f8d37d348f514f199528adb to your computer and use it in GitHub Desktop.
Convenient Swift abstractions for generating haptic feedback on iOS.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
or
or
Preparing Feedback
e.g.
Enabling & Disabling Haptics
Haptics are enabled by default.
Based on
UserDefaults
Replace with your own
UserDefaults
key.e.g.
or
Based on Constant Value
e.g.
or
Based on Closure
e.g.
or
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 acceptSemanticFeedback
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.
License
MIT