[DECKS-689] Make HapticsService reusable across projects (#1)
Work on xiiagency/Decks#689 Work on xiiagency/Xray#142
This commit is contained in:
parent
e1029141bb
commit
e998a94b92
|
@ -14,11 +14,19 @@ let package =
|
|||
targets: ["SwiftUIHaptics"]
|
||||
),
|
||||
],
|
||||
dependencies: [],
|
||||
dependencies: [
|
||||
.package(
|
||||
name: "SwiftFoundationExtensions",
|
||||
url: "https://github.com/xiiagency/SwiftFoundationExtensions",
|
||||
.branchItem("main")
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftUIHaptics",
|
||||
dependencies: []
|
||||
dependencies: [
|
||||
"SwiftFoundationExtensions"
|
||||
]
|
||||
),
|
||||
// NOTE: Re-enable when tests are added.
|
||||
// .testTarget(
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import CoreHaptics
|
||||
|
||||
/**
|
||||
A struct containing a series of `CHHapticEvent`s to be played via the `HapticsService`.
|
||||
*/
|
||||
public struct HapticEvent {
|
||||
/**
|
||||
The underlying events to be played.
|
||||
*/
|
||||
public let underlyingEvents: [CHHapticEvent]
|
||||
}
|
||||
|
||||
public extension HapticEvent {
|
||||
/**
|
||||
Creates a single `HapticEvent`, with the ability to specify all the parameters and defaulting others.
|
||||
*/
|
||||
static func single(
|
||||
intensity: Float? = nil,
|
||||
sharpness: Float? = nil,
|
||||
attack: Float? = nil,
|
||||
decay: Float? = nil,
|
||||
release: Float? = nil,
|
||||
sustained: Float? = nil,
|
||||
relativeTime: TimeInterval = 0.0,
|
||||
duration: TimeInterval = 0.0
|
||||
) -> HapticEvent {
|
||||
var hapticsParameters: [CHHapticEventParameter] = []
|
||||
if let intensity = intensity {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity)
|
||||
)
|
||||
}
|
||||
|
||||
if let sharpness = sharpness {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness)
|
||||
)
|
||||
}
|
||||
|
||||
if let attack = attack {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .attackTime, value: attack)
|
||||
)
|
||||
}
|
||||
|
||||
if let decay = decay {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .decayTime, value: decay)
|
||||
)
|
||||
}
|
||||
|
||||
if let release = release {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .releaseTime, value: release)
|
||||
)
|
||||
}
|
||||
|
||||
if let sustained = sustained {
|
||||
hapticsParameters.append(
|
||||
CHHapticEventParameter(parameterID: .sustained, value: sustained)
|
||||
)
|
||||
}
|
||||
|
||||
return HapticEvent(
|
||||
underlyingEvents: [
|
||||
CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: hapticsParameters,
|
||||
relativeTime: relativeTime,
|
||||
duration: duration
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a pattern of haptic events by combining all underlying events for the provided `HapticEvent` instances.
|
||||
*/
|
||||
static func pattern(events: [HapticEvent]) -> HapticEvent {
|
||||
let combinedUnderlyingEvents: [CHHapticEvent] = events.flatMap { event in
|
||||
event.underlyingEvents
|
||||
}
|
||||
|
||||
return HapticEvent(underlyingEvents: combinedUnderlyingEvents)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a pattern of haptic events by combining all underlying events for the provided `HapticEvent` instances.
|
||||
*/
|
||||
static func pattern(_ events: HapticEvent...) -> HapticEvent {
|
||||
pattern(events: events)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a time shifted `HapticEvent` with the provided relative time and play duration specified.
|
||||
*/
|
||||
func timeShifted(relativeTime: TimeInterval, duration: TimeInterval) -> HapticEvent {
|
||||
let shiftedUnderlyingEvents: [CHHapticEvent] = underlyingEvents.map { event in
|
||||
CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: event.eventParameters,
|
||||
relativeTime: relativeTime,
|
||||
duration: duration
|
||||
)
|
||||
}
|
||||
|
||||
return HapticEvent(underlyingEvents: shiftedUnderlyingEvents)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
import CoreHaptics
|
||||
import SwiftFoundationExtensions
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
/**
|
||||
A service that integrates to the device's haptics engine, if it is available.
|
||||
*/
|
||||
public class HapticsService : ObservableObject {
|
||||
private static let logger: Logger = .loggerFor(HapticsService.self)
|
||||
|
||||
/**
|
||||
True if the device supports haptic feedback, false otherwise.
|
||||
*/
|
||||
public private(set) var isAvailable: Bool = CHHapticEngine.capabilitiesForHardware()
|
||||
.supportsHaptics
|
||||
|
||||
/**
|
||||
The currently initialized haptics engine.
|
||||
*/
|
||||
private var currentEngine: CHHapticEngine? = nil
|
||||
|
||||
public init() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
deinit {
|
||||
deinitialize()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the service is initialized, subscribes to foreground/background notifications and initializes the haptics engine.
|
||||
|
||||
NOTE: Implemented as a separate fileprivate func in order to allow for the mock of this service to override the initialization process.
|
||||
*/
|
||||
fileprivate func initialize() {
|
||||
startReceivingAppStatusNotifications()
|
||||
setupEngine()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the service is de-initialized, unsubscribes to foreground/background notifications.
|
||||
|
||||
NOTE: Implemented as a separate fileprivate func in order to allow for the mock of this service to override the de-initialization process.
|
||||
*/
|
||||
fileprivate func deinitialize() {
|
||||
stopReceivingAppStatusNotifications()
|
||||
}
|
||||
|
||||
/**
|
||||
Called to subscribe to notifications of the app entering/exiting foreground so that we can start/shutdown the haptics engine to match.
|
||||
*/
|
||||
private func startReceivingAppStatusNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(setupEngine),
|
||||
name: UIApplication.willEnterForegroundNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(shutdownEngine),
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Called when de-initialized to unsubscribe from foreground/background notifications.
|
||||
*/
|
||||
private func stopReceivingAppStatusNotifications() {
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UIApplication.willEnterForegroundNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets up the device's haptic engine. Called when this service is initialized or whenever the app returns to foreground.
|
||||
*/
|
||||
@objc
|
||||
private func setupEngine() {
|
||||
shutdownEngine()
|
||||
|
||||
do {
|
||||
let engine = try CHHapticEngine()
|
||||
engine.start(
|
||||
completionHandler: { [self] error in
|
||||
if let error = error {
|
||||
Self.logger.error("Error starting haptics engine: \(error.description, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
currentEngine = engine
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
Self.logger.error("Error creating haptics engine: \(error.description, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Shuts down the haptic engine. Called whenever the app enters background.
|
||||
*/
|
||||
@objc
|
||||
private func shutdownEngine() {
|
||||
guard let engine = currentEngine else {
|
||||
return
|
||||
}
|
||||
|
||||
engine.stop(
|
||||
completionHandler: { [self] error in
|
||||
if let error = error {
|
||||
Self.logger.error("Error occurred shutting down haptics engine: \(error.description, privacy: .public)")
|
||||
}
|
||||
|
||||
currentEngine = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Plays a pattern of haptic events using the current haptics engine, if it is defined and haptics are supported.
|
||||
*/
|
||||
public func play(event: HapticEvent) {
|
||||
guard let engine = currentEngine else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let pattern = try CHHapticPattern(events: event.underlyingEvents, parameters: [])
|
||||
let player = try engine.makePlayer(with: pattern)
|
||||
try player.start(atTime: 0)
|
||||
} catch {
|
||||
Self.logger.error("Error attempting to play haptics pattern: \(error.description, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/**
|
||||
Provides the ability to create a "mock" `HapticsService` to be used in preview code.
|
||||
*/
|
||||
public class MockHapticsService : HapticsService {
|
||||
override func initialize() {
|
||||
// no-op, blocks default initializer.
|
||||
}
|
||||
|
||||
override func deinitialize() {
|
||||
// no-op, blocks default shutdown.
|
||||
}
|
||||
|
||||
public var isAvailableCallback: () -> Bool = { true }
|
||||
public var playCallback: (HapticEvent) -> Void = { _ in Void() }
|
||||
|
||||
public override var isAvailable: Bool {
|
||||
isAvailableCallback()
|
||||
}
|
||||
|
||||
public override func play(event: HapticEvent) {
|
||||
playCallback(event)
|
||||
}
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue