Skip to content

Commit 678fcb5

Browse files
Merge branch 'develop'
2 parents e204665 + 5243655 commit 678fcb5

6 files changed

Lines changed: 80 additions & 62 deletions

File tree

vibrator/Vibrator/Vibrator.swift

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import Foundation
99
import AudioToolbox
1010
import 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

vibrator/vibrator-demo/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import UIKit
99

1010
@UIApplicationMain
11-
class AppDelegate: UIResponder, UIApplicationDelegate {
11+
final class AppDelegate: UIResponder, UIApplicationDelegate {
1212

1313
// MARK: - UIApplicationDelegate
1414
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

vibrator/vibrator-demo/Extensions/UIDevice+Extensions.swift

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,12 @@ import UIKit
99

1010
extension UIDevice {
1111

12-
static let isSimulator = UIDevice.current.modelName == Model.simulator.rawValue
13-
14-
private var modelName: String {
15-
return name(for: modelIdentifier())
16-
}
17-
18-
private func modelIdentifier() -> String {
19-
var systemInfo: utsname = utsname()
20-
uname(&systemInfo)
21-
22-
let machineMirror: Mirror = Mirror(reflecting: systemInfo.machine)
23-
let identifier: String = machineMirror.children.reduce(String()) { identifier, element in
24-
guard
25-
let value: Int8 = element.value as? Int8,
26-
value != 0
27-
else { return identifier }
28-
29-
return identifier + String(UnicodeScalar(UInt8(value)))
30-
}
31-
32-
return identifier
33-
}
34-
35-
private func name(for modelIdentifier: String) -> String {
36-
switch modelIdentifier {
37-
case "x86_64": return Model.simulator.rawValue
38-
default: return modelIdentifier
39-
}
40-
}
41-
42-
private enum Model: String {
43-
case simulator = "Simulator"
44-
}
12+
static let isSimulator: Bool = {
13+
#if targetEnvironment(simulator)
14+
return true
15+
#else
16+
return false
17+
#endif
18+
}()
4519

4620
}

vibrator/vibrator-demo/Home/HomeViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import UIKit
99

10-
class HomeViewController: UIViewController {
10+
final class HomeViewController: UIViewController {
1111

1212
@IBOutlet private weak var deviceHapticSupportLabel: UILabel!
1313
@IBOutlet private weak var heartHapticButton: UIButton!

vibrator/vibrator-demo/SceneDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import UIKit
99

10-
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
10+
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
1111

1212
var window: UIWindow?
1313

vibrator/vibrator.xcodeproj/project.pbxproj

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 50;
6+
objectVersion = 54;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -157,8 +157,9 @@
157157
841E136D24216EB100940E53 /* Project object */ = {
158158
isa = PBXProject;
159159
attributes = {
160+
BuildIndependentTargetsInParallel = YES;
160161
LastSwiftUpdateCheck = 1130;
161-
LastUpgradeCheck = 1130;
162+
LastUpgradeCheck = 1640;
162163
ORGANIZATIONNAME = "Daniel Storm";
163164
TargetAttributes = {
164165
841E137424216EB200940E53 = {
@@ -252,6 +253,7 @@
252253
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
253254
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
254255
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
256+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
255257
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
256258
CLANG_WARN_STRICT_PROTOTYPES = YES;
257259
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -260,6 +262,7 @@
260262
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
261263
COPY_PHASE_STRIP = NO;
262264
DEBUG_INFORMATION_FORMAT = dwarf;
265+
DEVELOPMENT_TEAM = E5BK5K7RRN;
263266
ENABLE_STRICT_OBJC_MSGSEND = YES;
264267
ENABLE_TESTABILITY = YES;
265268
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -313,6 +316,7 @@
313316
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
314317
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
315318
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
319+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
316320
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
317321
CLANG_WARN_STRICT_PROTOTYPES = YES;
318322
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -321,6 +325,7 @@
321325
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
322326
COPY_PHASE_STRIP = NO;
323327
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
328+
DEVELOPMENT_TEAM = E5BK5K7RRN;
324329
ENABLE_NS_ASSERTIONS = NO;
325330
ENABLE_STRICT_OBJC_MSGSEND = YES;
326331
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -348,9 +353,8 @@
348353
CODE_SIGN_IDENTITY = "iPhone Developer";
349354
CODE_SIGN_STYLE = Manual;
350355
CURRENT_PROJECT_VERSION = 1;
351-
DEVELOPMENT_TEAM = E5BK5K7RRN;
352356
INFOPLIST_FILE = "vibrator-demo/Info.plist";
353-
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
357+
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
354358
LD_RUNPATH_SEARCH_PATHS = (
355359
"$(inherited)",
356360
"@executable_path/Frameworks",
@@ -371,9 +375,8 @@
371375
CODE_SIGN_IDENTITY = "iPhone Developer";
372376
CODE_SIGN_STYLE = Manual;
373377
CURRENT_PROJECT_VERSION = 1;
374-
DEVELOPMENT_TEAM = E5BK5K7RRN;
375378
INFOPLIST_FILE = "vibrator-demo/Info.plist";
376-
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
379+
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
377380
LD_RUNPATH_SEARCH_PATHS = (
378381
"$(inherited)",
379382
"@executable_path/Frameworks",

0 commit comments

Comments
 (0)