[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:
Gennadiy Shafranovich 2022-01-31 16:24:09 -05:00 committed by GitHub
parent e998a94b92
commit b2e36ae335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 105 deletions

View File

@ -7,6 +7,7 @@ let package =
platforms: [
.iOS(.v15),
.watchOS(.v8),
.macOS(.v12),
],
products: [
.library(

View File

@ -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

View File

@ -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

View File

@ -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)
}
}