[CAKER-2] Enable macOS target compatibility (#2)
- adjusted package platform list - separated AppKit/UIKit specific implementation into `HapticPlayer` component, used by a common service abstraction Work on xiiagency/Caker#2
This commit is contained in:
parent
e998a94b92
commit
b2e36ae335
|
@ -7,6 +7,7 @@ let package =
|
|||
platforms: [
|
||||
.iOS(.v15),
|
||||
.watchOS(.v8),
|
||||
.macOS(.v12),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#if os(macOS)
|
||||
|
||||
import SwiftFoundationExtensions
|
||||
import os
|
||||
|
||||
/**
|
||||
A macOS implementation of the haptics player.
|
||||
|
||||
NOTE: Currently a no-op that always returns false for `isAvailable` and does nothing when starting/stopping or playing an event.
|
||||
*/
|
||||
class HapticsPlayer {
|
||||
private static let logger: Logger = .loggerFor(HapticsPlayer.self)
|
||||
|
||||
/**
|
||||
NOTE: We currently do not support haptics feedback on macOS.
|
||||
*/
|
||||
static let isAvailable: Bool = false
|
||||
|
||||
/**
|
||||
NOTE: We currently do not support haptics feedback on macOS, this is a no-op.
|
||||
*/
|
||||
func start() {}
|
||||
|
||||
/**
|
||||
NOTE: We currently do not support haptics feedback on macOS, this is a no-op.
|
||||
*/
|
||||
func stop() {}
|
||||
|
||||
/**
|
||||
NOTE: We currently do not support haptics feedback on macOS.
|
||||
*/
|
||||
func play(event: HapticEvent) {
|
||||
Self.logger.warning("No haptics support on MacOS.")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,139 @@
|
|||
#if !os(macOS)
|
||||
|
||||
import CoreHaptics
|
||||
import SwiftFoundationExtensions
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
/**
|
||||
The UIKit implementation of the `HapticsPlayer`, utilizing CoreHaptics.
|
||||
*/
|
||||
class HapticsPlayer {
|
||||
private static let logger: Logger = .loggerFor(HapticsPlayer.self)
|
||||
|
||||
/**
|
||||
Returns true if the device supports haptic feedback, false otherwise.
|
||||
*/
|
||||
static var isAvailable: Bool {
|
||||
CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
||||
}
|
||||
|
||||
/**
|
||||
The currently initialized haptics engine.
|
||||
*/
|
||||
private var currentEngine: CHHapticEngine? = nil
|
||||
|
||||
/**
|
||||
Subscribes to foreground/background notifications and initializes the haptics engine.
|
||||
*/
|
||||
func start() {
|
||||
startReceivingAppStatusNotifications()
|
||||
setupEngine()
|
||||
}
|
||||
|
||||
/**
|
||||
Unsubscribes to foreground/background notifications.
|
||||
*/
|
||||
func stop() {
|
||||
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.
|
||||
*/
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -1,24 +1,25 @@
|
|||
import CoreHaptics
|
||||
import SwiftFoundationExtensions
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
/**
|
||||
A service that integrates to the device's haptics engine, if it is available.
|
||||
|
||||
NOTE: The haptics feedback are only implemented for iOS/watchOS, but not on macOS.
|
||||
The play function will not produce feedback and will return control right away.
|
||||
*/
|
||||
public class HapticsService : ObservableObject {
|
||||
private static let logger: Logger = .loggerFor(HapticsService.self)
|
||||
|
||||
/**
|
||||
True if the device supports haptic feedback, false otherwise.
|
||||
Return true if the device supports haptic feedback, false otherwise.
|
||||
*/
|
||||
public private(set) var isAvailable: Bool = CHHapticEngine.capabilitiesForHardware()
|
||||
.supportsHaptics
|
||||
public var isAvailable: Bool {
|
||||
HapticsPlayer.isAvailable
|
||||
}
|
||||
|
||||
/**
|
||||
The currently initialized haptics engine.
|
||||
*/
|
||||
private var currentEngine: CHHapticEngine? = nil
|
||||
// The underlying, platform-specific, haptics player.
|
||||
private let player: HapticsPlayer = HapticsPlayer()
|
||||
|
||||
public init() {
|
||||
initialize()
|
||||
|
@ -29,119 +30,28 @@ public class HapticsService : ObservableObject {
|
|||
}
|
||||
|
||||
/**
|
||||
Called when the service is initialized, subscribes to foreground/background notifications and initializes the haptics engine.
|
||||
Called when the service is initialized, initializes the `HapticsPlayer` for the current platform.
|
||||
|
||||
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()
|
||||
player.start()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the service is de-initialized, unsubscribes to foreground/background notifications.
|
||||
Called when the service is de-initialized, de-initializes the `HapticsPlayer` for the current platform.
|
||||
|
||||
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()
|
||||
player.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
Plays a pattern of haptic events using the current haptics player, if 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)")
|
||||
}
|
||||
player.play(event: event)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue