[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: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v15),
|
||||||
.watchOS(.v8),
|
.watchOS(.v8),
|
||||||
|
.macOS(.v12),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.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 SwiftFoundationExtensions
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import os
|
import os
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A service that integrates to the device's haptics engine, if it is available.
|
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 {
|
public class HapticsService : ObservableObject {
|
||||||
private static let logger: Logger = .loggerFor(HapticsService.self)
|
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()
|
public var isAvailable: Bool {
|
||||||
.supportsHaptics
|
HapticsPlayer.isAvailable
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// The underlying, platform-specific, haptics player.
|
||||||
The currently initialized haptics engine.
|
private let player: HapticsPlayer = HapticsPlayer()
|
||||||
*/
|
|
||||||
private var currentEngine: CHHapticEngine? = nil
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
initialize()
|
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.
|
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() {
|
fileprivate func initialize() {
|
||||||
startReceivingAppStatusNotifications()
|
player.start()
|
||||||
setupEngine()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
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.
|
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() {
|
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.
|
Plays a pattern of haptic events using the current haptics player, if haptics are supported.
|
||||||
*/
|
|
||||||
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) {
|
public func play(event: HapticEvent) {
|
||||||
guard let engine = currentEngine else {
|
player.play(event: event)
|
||||||
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)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue