@@ -9,8 +9,8 @@ import Foundation
99import AudioToolbox
1010import CoreHaptics
1111
12- /// A class that allows your app to play system vibrations and Apple Haptic and Audio Pattern (AHAP) files generated with [Lofelt Composer](https://composer.lofelt.com) .
13- public class Vibrator {
12+ /// A class that allows your app to play system vibrations and Apple Haptic and Audio Pattern (AHAP) files.
13+ public final class Vibrator {
1414
1515 /// Options for device vibration rates when looping.
1616 public enum Frequency {
@@ -25,6 +25,10 @@ public class Vibrator {
2525 }
2626 }
2727
28+ /// Enables/disables logging to the console in a `#DEBUG` environment.
29+ /// Default value is `false`.
30+ public static var isDebugLoggingEnabled : Bool = false
31+
2832 /// Indicates if the device supports haptic event playback.
2933 public let supportsHaptics : Bool = {
3034 return CHHapticEngine . capabilitiesForHardware ( ) . supportsHaptics
@@ -42,7 +46,6 @@ public class Vibrator {
4246 }
4347
4448 private var hapticPlayer : CHHapticPatternPlayer ?
45-
4649 private var vibrateLoopTimer : Timer ?
4750 private var hapticLoopTimer : Timer ?
4851
@@ -51,13 +54,15 @@ public class Vibrator {
5154 public static let shared : Vibrator = Vibrator ( )
5255 private init ( ) {
5356 guard supportsHaptics else { return }
54- hapticEngine = try ? CHHapticEngine ( )
57+ do { hapticEngine = try CHHapticEngine ( ) }
58+ catch { log ( " \( #function) -> Could not create haptic engine: \( error) " ) }
5559 }
5660
5761 /// Prepares the vibrator by acquiring hardware needed for vibrations.
5862 public func prepare( ) {
5963 guard let hapticEngine: CHHapticEngine = hapticEngine else { return }
60- try ? hapticEngine. start ( )
64+ do { try hapticEngine. start ( ) }
65+ catch { log ( " \( #function) -> Could not start haptic engine: \( error) " ) }
6166 }
6267
6368 // MARK: - Vibrate
@@ -102,7 +107,7 @@ public class Vibrator {
102107 guard
103108 let timer: Timer = vibrateLoopTimer,
104109 timer. isValid
105- else { return }
110+ else { return }
106111
107112 timer. invalidate ( )
108113 vibrateLoopTimer = nil
@@ -126,40 +131,74 @@ public class Vibrator {
126131 /// Has no effect if `loop` is `false` when starting the haptic.
127132 public func stopHaptic( ) {
128133 stopHapticLoopTimer ( )
129- try ? hapticPlayer? . stop ( atTime: CHHapticTimeImmediate)
134+ do { try hapticPlayer? . stop ( atTime: CHHapticTimeImmediate) }
135+ catch { log ( " \( #function) -> Could not stop haptic engine: \( error) " ) }
130136 hapticPlayer = nil
131137 }
132138
133139 private func playHaptic( named filename: String ) {
134140 guard
135141 let hapticEngine: CHHapticEngine = hapticEngine,
136142 let hapticPath: String = Bundle . main. path ( forResource: filename, ofType: AppleHapticAudioPattern . fileExtension)
137- else { return }
143+ else { return }
144+
145+ do { try hapticEngine. start ( ) }
146+ catch { log ( " \( #function) -> Could not start haptic engine: \( error) " ) }
138147
139- try ? hapticEngine. start ( )
140- try ? hapticEngine . playPattern ( from : URL ( fileURLWithPath : hapticPath ) )
148+ do { try hapticEngine. playPattern ( from : URL ( fileURLWithPath : hapticPath ) ) }
149+ catch { log ( " \( #function ) -> Could not play pattern: \( error ) " ) }
141150 }
142151
143152 private func playHapticLoop( named filename: String ) {
144153 guard
145154 let hapticEngine: CHHapticEngine = hapticEngine,
146155 let hapticPath: String = Bundle . main. path ( forResource: filename, ofType: AppleHapticAudioPattern . fileExtension) ,
147- let hapticData: Data = try ? Data ( contentsOf : URL ( fileURLWithPath : hapticPath) ) ,
156+ let hapticData: Data = hapticData ( hapticPath : hapticPath) ,
148157 let appleHapticAudioPattern: AppleHapticAudioPattern = AppleHapticAudioPattern ( data: hapticData) ,
149158 let appleHapticAudioPatternDictionary: [ CHHapticPattern . Key : Any ] = appleHapticAudioPattern. dictionaryRepresentation ( ) ,
150159 let hapticDuration: TimeInterval = appleHapticAudioPattern. pattern? . first ( where: { $0. event? . eventDuration != nil } ) ? . event? . eventDuration,
151- let hapticPattern: CHHapticPattern = try ? CHHapticPattern ( dictionary: appleHapticAudioPatternDictionary) ,
152- let hapticPlayer: CHHapticPatternPlayer = try ? hapticEngine. makePlayer ( with: hapticPattern)
153- else { return }
160+ let hapticPattern: CHHapticPattern = hapticPattern ( appleHapticAudioPatternDictionary: appleHapticAudioPatternDictionary) ,
161+ let hapticPlayer: CHHapticPatternPlayer = hapticPlayer ( hapticPattern: hapticPattern)
162+ else { return }
163+
164+ do { try hapticEngine. start ( ) }
165+ catch { log ( " \( #function) -> Could not start haptic engine: \( error) " ) }
154166
155- try ? hapticEngine. start ( )
156167 self . hapticPlayer = hapticPlayer
157- try ? self . hapticPlayer? . start ( atTime: CHHapticTimeImmediate)
168+
169+ do { try self . hapticPlayer? . start ( atTime: CHHapticTimeImmediate) }
170+ catch { log ( " \( #function) -> Could not start haptic player at time CHHapticTimeImmediate: \( error) " ) }
171+
158172 startHapticLoopTimer ( timeInterval: hapticDuration)
159173 }
160174
175+ private func hapticData( hapticPath: String ) -> Data ? {
176+ var hapticData : Data ?
177+ do { hapticData = try Data ( contentsOf: URL ( fileURLWithPath: hapticPath) ) }
178+ catch { log ( " \( #function) -> Could not load haptic data: \( error) " ) }
179+ return hapticData
180+ }
181+
182+ private func hapticPattern( appleHapticAudioPatternDictionary: [ CHHapticPattern . Key : Any ] ) -> CHHapticPattern ? {
183+ var hapticPattern : CHHapticPattern ?
184+ do { hapticPattern = try CHHapticPattern ( dictionary: appleHapticAudioPatternDictionary) }
185+ catch { log ( " \( #function) -> Could not create haptic pattern: \( error) " ) }
186+ return hapticPattern
187+ }
188+
189+ private func hapticPlayer( hapticPattern: CHHapticPattern ) -> CHHapticPatternPlayer ? {
190+ var hapticPlayer : CHHapticPatternPlayer ?
191+ // Intentionally creating an advanced player, `CHHapticAdvancedPatternPlayer`, with `.makeAdvancedPlayer` as of iOS 18 here.
192+ // Using a standard player, `CHHapticPatternPlayer`, will throw a `CHHapticError.Code.memoryError` `com.apple.CoreHaptics error -4899`.
193+ // Apparently advanced players can play larger haptic patterns than standard ones, but that isn't officially documented anywhere...
194+ do { hapticPlayer = try hapticEngine? . makeAdvancedPlayer ( with: hapticPattern) }
195+ catch { log ( " \( #function) -> Could not create haptic player: \( error) " ) }
196+ return hapticPlayer
197+ }
198+
161199 @objc private func restartHapticPlayer( ) {
162- try ? hapticPlayer? . start ( atTime: 0.0 )
200+ do { try self . hapticPlayer? . start ( atTime: 0.0 ) }
201+ catch { log ( " \( #function) -> Could not start haptic player at time 0.0: \( error) " ) }
163202 }
164203
165204 private func startHapticLoopTimer( timeInterval: TimeInterval ) {
@@ -175,7 +214,7 @@ public class Vibrator {
175214 guard
176215 let timer: Timer = hapticLoopTimer,
177216 timer. isValid
178- else { return }
217+ else { return }
179218
180219 timer. invalidate ( )
181220 hapticLoopTimer = nil
@@ -189,7 +228,8 @@ public class Vibrator {
189228 /// Called when the haptic engine fails. Will attempt to restart the haptic engine.
190229 private func hapticEngineDidRecoverFromServerError( ) {
191230 log ( " \( #function) " )
192- try ? hapticEngine? . start ( )
231+ do { try hapticEngine? . start ( ) }
232+ catch { log ( " \( #function) -> Could not start haptic engine: \( error) " ) }
193233 }
194234
195235}
@@ -199,7 +239,8 @@ private extension Vibrator {
199239 // MARK: - Logging
200240 func log( _ message: String ) {
201241 #if DEBUG
202- print ( " \n 📳 \( String ( describing: Vibrator . self) ) : \( #function) -> message: \( message) \n " )
242+ guard Vibrator . isDebugLoggingEnabled else { return }
243+ print ( " 📳 \( String ( describing: Vibrator . self) ) -> message: \( message) " )
203244 #endif
204245 }
205246
0 commit comments