From e3a2f8b265209b6241786ccf8968ad7af90a7b3c Mon Sep 17 00:00:00 2001 From: Kirill Kostarev Date: Sat, 27 Apr 2024 13:38:31 +0300 Subject: [PATCH 1/2] Improved and updated realization by the standards of the book --- .../AccessibilityFeedbackEngine.swift | 71 +++++++++++++++++ .../AccessibilityFeedbackPlayer.swift | 61 ++++++++++++++ .../AccessibilityHapticEngine.swift | 40 ++++++++++ .../AccessibleActivityIndicatorView.swift | 23 ++++++ .../AccessibleLoading/AccessibleLoading.swift | 37 --------- Sources/AccessibleLoading/Config.swift | 23 ------ Sources/AccessibleLoading/HapticPlayer.swift | 71 ----------------- Sources/AccessibleLoading/PatternFabric.swift | 79 ------------------- 8 files changed, 195 insertions(+), 210 deletions(-) create mode 100644 Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift create mode 100644 Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift create mode 100644 Sources/AccessibleLoading/AccessibilityHapticEngine.swift create mode 100644 Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift delete mode 100644 Sources/AccessibleLoading/AccessibleLoading.swift delete mode 100644 Sources/AccessibleLoading/Config.swift delete mode 100644 Sources/AccessibleLoading/HapticPlayer.swift delete mode 100644 Sources/AccessibleLoading/PatternFabric.swift diff --git a/Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift b/Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift new file mode 100644 index 0000000..a8f0a06 --- /dev/null +++ b/Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift @@ -0,0 +1,71 @@ +import CoreHaptics +import Foundation +import UIKit + +@available(iOS 13.0, *) +final class AccessibilityFeedbackEngine { + + // MARK: - Internal Methods + + func start(duration: TimeInterval) -> CHHapticPattern? { + guard UIAccessibility.isVoiceOverRunning else { + return nil + } + let events = makeEvents(duration: duration) + return try? CHHapticPattern(events: events, parameterCurves: []) + } + + func stop() -> CHHapticPattern? { + guard UIAccessibility.isVoiceOverRunning else { + return nil + } + let event = makeStopEvent() + return try? CHHapticPattern(events: event, parameterCurves: []) + } + + // MARK: - Private Types + + private struct Event { + let intensity: Float + let sharpness: Float + } + + private enum Constants { + static let eventsPerSecond = 1 + static let standardEvent = Event(intensity: 0.4, sharpness: 0.5) + static let penultimateEvent = Event(intensity: 0.5, sharpness: 0.5) + static let finalEvent = Event(intensity: 0.75, sharpness: 0) + static let delayToFinalEvent: TimeInterval = 0.2 + } + + // MARK: - Private Methods + + private func makeEvents(duration: TimeInterval) -> [CHHapticEvent] { + let interval = 1 / TimeInterval(Constants.eventsPerSecond) + return (0.. CHHapticEvent { + return makeEvent(with: Constants.standardEvent, time: time) + } + + private func makeStopEvent() -> [CHHapticEvent] { + let beforeFinalEvent = makeEvent(with: Constants.penultimateEvent, time: 0) + let finalEvent = makeEvent(with: Constants.finalEvent, time: Constants.delayToFinalEvent) + return [beforeFinalEvent, finalEvent] + } + + private func makeEvent(with event: Event, time: TimeInterval) -> CHHapticEvent { + let intensityParameter = CHHapticEventParameter(parameterID: .hapticIntensity, value: event.intensity) + let sharpnessParameter = CHHapticEventParameter(parameterID: .hapticSharpness, value: event.sharpness) + return CHHapticEvent( + eventType: .hapticTransient, + parameters: [intensityParameter, sharpnessParameter], + relativeTime: time + ) + } + +} diff --git a/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift b/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift new file mode 100644 index 0000000..44aaee2 --- /dev/null +++ b/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift @@ -0,0 +1,61 @@ +import Foundation +import CoreHaptics + +@available(iOS 13.0, *) +final class AccessibilityFeedbackPlayer { + + // MARK: - Internal Init + + init?() { + guard supportsHapticFeedback else { + return nil + } + + engine = try? CHHapticEngine() + try? engine?.start() + + engine?.stoppedHandler = { [weak self] _ in + self?.needReset = true + } + + engine?.resetHandler = { [weak self] in + self?.needReset = true + } + } + + // MARK: - Internal Methods + + func play(pattern: CHHapticPattern?) { + guard let pattern else { + return + } + resetIfNeeded() + player = try? engine?.makePlayer(with: pattern) + try? player?.start(atTime: CHHapticTimeImmediate) + } + + func stop(afterStop: (() -> Void)?) { + try? player?.stop(atTime: CHHapticTimeImmediate) + afterStop?() + player = nil + } + + // MARK: - Private Properties + + private var engine: CHHapticEngine? + private var player: CHHapticPatternPlayer? + private var needReset = false + + private var supportsHapticFeedback: Bool { + return CHHapticEngine.capabilitiesForHardware().supportsHaptics + } + + // MARK: - Private Methods + + private func resetIfNeeded() { + guard needReset else { return } + defer { needReset = false } + try? engine?.start() + } + +} diff --git a/Sources/AccessibleLoading/AccessibilityHapticEngine.swift b/Sources/AccessibleLoading/AccessibilityHapticEngine.swift new file mode 100644 index 0000000..e478851 --- /dev/null +++ b/Sources/AccessibleLoading/AccessibilityHapticEngine.swift @@ -0,0 +1,40 @@ +import UIKit +import Foundation + +@available(iOS 13.0, *) +public final class AccessibilityHapticEngine { + + // MARK: - Public Init + + public init() {} + + // MARK: - Public Methods + + public func start(duration: TimeInterval = TimeInterval(120)) { + Task { @MainActor [weak self] in + guard let self else { + return + } + guard !isStarted else { return } + isStarted = true + try? await Task.sleep(nanoseconds: UInt64(0.5 * 1_000_000_000)) + guard let pattern = feedbackEngine.start(duration: duration) else { return } + feedbackPlayer?.play(pattern: pattern) + } + } + + public func stop() { + feedbackPlayer?.stop { [weak self] in + self?.isStarted = false + guard let pattern = self?.feedbackEngine.stop() else { return } + self?.feedbackPlayer?.play(pattern: pattern) + } + } + + // MARK: - Private Properties + + private var isStarted = false + private lazy var feedbackEngine = AccessibilityFeedbackEngine() + private lazy var feedbackPlayer = AccessibilityFeedbackPlayer() + +} diff --git a/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift b/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift new file mode 100644 index 0000000..5a390a4 --- /dev/null +++ b/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift @@ -0,0 +1,23 @@ +import UIKit +import Foundation + +@available(iOS 13.0, *) +public final class AccessibleActivityIndicatorView: UIActivityIndicatorView { + + // MARK: - Public Methods + + public override func startAnimating() { + super.startAnimating() + feedbackEngine.start() + } + + public override func stopAnimating() { + super.stopAnimating() + feedbackEngine.stop() + } + + // MARK: - Private Properties + + private let feedbackEngine = AccessibilityHapticEngine() + +} diff --git a/Sources/AccessibleLoading/AccessibleLoading.swift b/Sources/AccessibleLoading/AccessibleLoading.swift deleted file mode 100644 index cb997bb..0000000 --- a/Sources/AccessibleLoading/AccessibleLoading.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -@available(iOS 13.0, *) -public class AccessibleLoading { - private let player = HapticPlayer() - - public init() {} - - public func start(duration: TimeInterval) { - player.play(pattern: PatternFabric().pattern(duration: duration)) - } - - public func stop() { - player.play(pattern: PatternFabric().finalPattern()) - } -} - -import UIKit -@available(iOS 13.0, *) -open class AccessibleActivityIndicatorView: UIActivityIndicatorView { - - let duration: TimeInterval = 20 - - let haptic = AccessibleLoading() - - open override func startAnimating() { - super.startAnimating() - - haptic.start(duration: duration) - } - - open override func stopAnimating() { - super.stopAnimating() - haptic.stop() - } -} - diff --git a/Sources/AccessibleLoading/Config.swift b/Sources/AccessibleLoading/Config.swift deleted file mode 100644 index f66db7e..0000000 --- a/Sources/AccessibleLoading/Config.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Config.swift -// -// -// Created by Mikhail Rubanov on 04.04.2021. -// - -import Foundation - -struct Config { - static let countsPerSec = 1 - - static let regular = Event(intensity: 0.4, sharpness: 0.5) - - static let prelast = Event(intensity: 0.5, sharpness: 0.5) - static let delayBetweenLast: TimeInterval = 0.2 - static let last = Event(intensity: 0.75, sharpness: 0) -} - -struct Event { - let intensity: Float - let sharpness: Float -} diff --git a/Sources/AccessibleLoading/HapticPlayer.swift b/Sources/AccessibleLoading/HapticPlayer.swift deleted file mode 100644 index d72c875..0000000 --- a/Sources/AccessibleLoading/HapticPlayer.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// HapticPlayer.swift -// -// -// Created by Mikhail Rubanov on 04.04.2021. -// - -import CoreHaptics -import os - -@available(iOS 13.0, *) -public class HapticPlayer { - public init() { - do { - engine = try CHHapticEngine() - try engine?.start() - engine?.stoppedHandler = { [weak self] stoppedReason in - guard let self = self else { return } - os_log(.error, log: self.logger, "Engine stopped by reason %d", stoppedReason.rawValue) - - self.needResetOnNextPlay = true - } - - engine?.resetHandler = { [weak self] in - guard let self = self else { return } - os_log(.error, log: self.logger, "Reset engine") - - do { - try self.engine?.start() - } catch { - os_log(.error, log: self.logger, "Can't create engine on reset", error.localizedDescription) - } - } - } catch let error { - os_log(.error, log: logger, "Can't create engine or start", error.localizedDescription) - } - } - - private var needResetOnNextPlay = false - - public func play(pattern: CHHapticPattern) { - resetIfNeeded() - - do { - player = try engine?.makePlayer(with: pattern) - - try player?.start(atTime: 0) - } catch let error { - os_log(.error, log: logger, "Can't play event", error.localizedDescription) - } - } - - private func resetIfNeeded() { - try? player?.cancel() - - if needResetOnNextPlay { - do { - try self.engine?.start() - needResetOnNextPlay = false - } catch { - os_log(.error, log: self.logger, "Can't reset engine on next play") - } - } - } - - private let logger = OSLog(subsystem: "com.akaDuality.HapticComposer", category: "player") - private var engine: CHHapticEngine? = nil - private var player: CHHapticPatternPlayer? -} - - diff --git a/Sources/AccessibleLoading/PatternFabric.swift b/Sources/AccessibleLoading/PatternFabric.swift deleted file mode 100644 index a201c63..0000000 --- a/Sources/AccessibleLoading/PatternFabric.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// PatternFabric.swift -// -// -// Created by Mikhail Rubanov on 04.04.2021. -// - -import CoreHaptics - -@available(iOS 13.0, *) -class PatternFabric { - - func pattern(duration: TimeInterval) -> CHHapticPattern { - var events = self.events(countPerSec: Config.countsPerSec, duration: duration) -// events.append(contentsOf: finalEvent(time: duration)) - - let pattern = try! CHHapticPattern( - events: events, - parameterCurves: []) - - return pattern - } - - func finalPattern() -> CHHapticPattern { - let pattern = try! CHHapticPattern( - events: finalEvent(time: 0), - parameterCurves: []) - - return pattern - } - - func events(countPerSec: Int, duration: TimeInterval) -> [CHHapticEvent] { - let interval = 1 / TimeInterval(countPerSec) - var events = [CHHapticEvent]() - - let times = Int(duration) * countPerSec - for index in 0.. [CHHapticEvent] { - [CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, - value: Config.prelast.intensity), - CHHapticEventParameter(parameterID: .hapticSharpness, - value: Config.prelast.sharpness), - ], - relativeTime: time), - CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, - value: Config.last.intensity), - CHHapticEventParameter(parameterID: .hapticSharpness, - value: Config.last.sharpness), - ], - relativeTime: time + Config.delayBetweenLast)] - } - - func eventWith(time: TimeInterval) -> CHHapticEvent { - let event = CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, - value: Config.regular.intensity), - CHHapticEventParameter(parameterID: .hapticSharpness, - value: Config.regular.sharpness)], - relativeTime: time) - return event - } -} - From c837afdfbc5e1e4cd8f80d6627fd70c1d7f19c49 Mon Sep 17 00:00:00 2001 From: Kirill Kostarev Date: Sat, 10 Aug 2024 20:00:27 +0300 Subject: [PATCH 2/2] New realization --- .../AccessibilityFeedbackPlayer.swift | 61 -------------- .../AccessibilityHapticEngine.swift | 40 --------- .../AccessibleActivityIndicatorView.swift | 6 +- .../Haptic/AccessibilityFeedbackPlayer.swift | 53 ++++++++++++ .../Haptic/AccessibilityHapticEngine.swift | 83 +++++++++++++++++++ .../AccessibilityHapticPatternFactory.swift} | 39 +++++---- .../Helpers/DelayedWorkItemExecutor.swift | 27 ++++++ 7 files changed, 188 insertions(+), 121 deletions(-) delete mode 100644 Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift delete mode 100644 Sources/AccessibleLoading/AccessibilityHapticEngine.swift create mode 100644 Sources/AccessibleLoading/Haptic/AccessibilityFeedbackPlayer.swift create mode 100644 Sources/AccessibleLoading/Haptic/AccessibilityHapticEngine.swift rename Sources/AccessibleLoading/{AccessibilityFeedbackEngine.swift => Haptic/AccessibilityHapticPatternFactory.swift} (50%) create mode 100644 Sources/AccessibleLoading/Haptic/Helpers/DelayedWorkItemExecutor.swift diff --git a/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift b/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift deleted file mode 100644 index 44aaee2..0000000 --- a/Sources/AccessibleLoading/AccessibilityFeedbackPlayer.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -import CoreHaptics - -@available(iOS 13.0, *) -final class AccessibilityFeedbackPlayer { - - // MARK: - Internal Init - - init?() { - guard supportsHapticFeedback else { - return nil - } - - engine = try? CHHapticEngine() - try? engine?.start() - - engine?.stoppedHandler = { [weak self] _ in - self?.needReset = true - } - - engine?.resetHandler = { [weak self] in - self?.needReset = true - } - } - - // MARK: - Internal Methods - - func play(pattern: CHHapticPattern?) { - guard let pattern else { - return - } - resetIfNeeded() - player = try? engine?.makePlayer(with: pattern) - try? player?.start(atTime: CHHapticTimeImmediate) - } - - func stop(afterStop: (() -> Void)?) { - try? player?.stop(atTime: CHHapticTimeImmediate) - afterStop?() - player = nil - } - - // MARK: - Private Properties - - private var engine: CHHapticEngine? - private var player: CHHapticPatternPlayer? - private var needReset = false - - private var supportsHapticFeedback: Bool { - return CHHapticEngine.capabilitiesForHardware().supportsHaptics - } - - // MARK: - Private Methods - - private func resetIfNeeded() { - guard needReset else { return } - defer { needReset = false } - try? engine?.start() - } - -} diff --git a/Sources/AccessibleLoading/AccessibilityHapticEngine.swift b/Sources/AccessibleLoading/AccessibilityHapticEngine.swift deleted file mode 100644 index e478851..0000000 --- a/Sources/AccessibleLoading/AccessibilityHapticEngine.swift +++ /dev/null @@ -1,40 +0,0 @@ -import UIKit -import Foundation - -@available(iOS 13.0, *) -public final class AccessibilityHapticEngine { - - // MARK: - Public Init - - public init() {} - - // MARK: - Public Methods - - public func start(duration: TimeInterval = TimeInterval(120)) { - Task { @MainActor [weak self] in - guard let self else { - return - } - guard !isStarted else { return } - isStarted = true - try? await Task.sleep(nanoseconds: UInt64(0.5 * 1_000_000_000)) - guard let pattern = feedbackEngine.start(duration: duration) else { return } - feedbackPlayer?.play(pattern: pattern) - } - } - - public func stop() { - feedbackPlayer?.stop { [weak self] in - self?.isStarted = false - guard let pattern = self?.feedbackEngine.stop() else { return } - self?.feedbackPlayer?.play(pattern: pattern) - } - } - - // MARK: - Private Properties - - private var isStarted = false - private lazy var feedbackEngine = AccessibilityFeedbackEngine() - private lazy var feedbackPlayer = AccessibilityFeedbackPlayer() - -} diff --git a/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift b/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift index 5a390a4..f1066b5 100644 --- a/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift +++ b/Sources/AccessibleLoading/AccessibleActivityIndicatorView.swift @@ -8,16 +8,16 @@ public final class AccessibleActivityIndicatorView: UIActivityIndicatorView { public override func startAnimating() { super.startAnimating() - feedbackEngine.start() + accessibilityHapticEngine?.startActivity() } public override func stopAnimating() { super.stopAnimating() - feedbackEngine.stop() + accessibilityHapticEngine?.stopActivity() } // MARK: - Private Properties - private let feedbackEngine = AccessibilityHapticEngine() + private let accessibilityHapticEngine = AccessibilityHapticEngine() } diff --git a/Sources/AccessibleLoading/Haptic/AccessibilityFeedbackPlayer.swift b/Sources/AccessibleLoading/Haptic/AccessibilityFeedbackPlayer.swift new file mode 100644 index 0000000..11ccec4 --- /dev/null +++ b/Sources/AccessibleLoading/Haptic/AccessibilityFeedbackPlayer.swift @@ -0,0 +1,53 @@ +import Foundation +import CoreHaptics + +@available(iOS 13.0, *) +final class AccessibilityFeedbackPlayer { + + // MARK: - Internal Init + + init?() { + do { + engine = try CHHapticEngine() + } catch { + return nil + } + } + + // MARK: - Internal Methods + + func playLoop(pattern: CHHapticPattern) { + do { + try engine.start() + player = try engine.makeAdvancedPlayer(with: pattern) + player?.loopEnabled = true + try player?.start(atTime: CHHapticTimeImmediate) + } catch { + assertionFailure("Accessibility feedback player unable to start pattern") + return + } + } + + func playOnce(pattern: CHHapticPattern) { + do { + try engine.start() + player = try engine.makeAdvancedPlayer(with: pattern) + try player?.start(atTime: CHHapticTimeImmediate) + } catch { + assertionFailure("Accessibility feedback player unable to start pattern") + return + } + } + + func stop(afterStop: (() -> Void)?) { + try? player?.stop(atTime: CHHapticTimeImmediate) + player = nil + afterStop?() + } + + // MARK: - Private Properties + + private var engine: CHHapticEngine + private var player: CHHapticAdvancedPatternPlayer? + +} diff --git a/Sources/AccessibleLoading/Haptic/AccessibilityHapticEngine.swift b/Sources/AccessibleLoading/Haptic/AccessibilityHapticEngine.swift new file mode 100644 index 0000000..e3af9a3 --- /dev/null +++ b/Sources/AccessibleLoading/Haptic/AccessibilityHapticEngine.swift @@ -0,0 +1,83 @@ +import CoreHaptics +import Foundation +import UIKit + +/// AccessibilityHapticEngine is a class designed to provide haptic feedback for accessibility purposes. +/// It utilizes the Core Haptics framework to create and manage haptic patterns that can be used to indicate loading states or +/// other activities. +/// +/// Usage: +/// ``` +/// let hapticEngine = AccessibilityHapticEngine() +/// hapticEngine?.startActivity() +/// // Some time later... +/// hapticEngine?.stopActivity() +/// ``` +@available(iOS 13.0, *) +public final class AccessibilityHapticEngine { + + // MARK: - Public Init + + /// Initializes the haptic engine. + /// Returns nil if the device does not support haptic feedback. + public init?() { + guard supportsHapticFeedback else { + return nil + } + } + + // MARK: - Public Methods + + /// Starts the haptic feedback loading activity. + /// If there is an existing activity, this method does nothing. + public func startActivity() { + guard activityDelayedWorkItem == nil else { + return + } + activityDelayedWorkItem = DelayedWorkItemExecutor(delay: Constants.delay) { [weak self] in + guard let self else { + return + } + if let pattern = patternFactory.makeOnStartActivityPattern(duration: Constants.duration) { + feedbackPlayer?.playLoop(pattern: pattern) + } + } + } + + /// Stops the ongoing haptic feedback activity and plays a stop activity pattern. + public func stopActivity() { + guard activityDelayedWorkItem != nil else { + return + } + activityDelayedWorkItem = nil + feedbackPlayer?.stop { [weak self] in + guard let self else { + return + } + if let pattern = patternFactory.makeOnStopActivityPattern() { + feedbackPlayer?.playOnce(pattern: pattern) + } + } + } + + // MARK: - Private Types + + private enum Constants { + + static var duration: TimeInterval { TimeInterval(60) } + static var delay: Double { 0.5 } + + } + + // MARK: - Private Properties + + private lazy var patternFactory = AccessibilityHapticPatternFactory() + private lazy var feedbackPlayer = AccessibilityFeedbackPlayer() + + private var activityDelayedWorkItem: DelayedWorkItemExecutor? + + private var supportsHapticFeedback: Bool { + return CHHapticEngine.capabilitiesForHardware().supportsHaptics + } + +} diff --git a/Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift b/Sources/AccessibleLoading/Haptic/AccessibilityHapticPatternFactory.swift similarity index 50% rename from Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift rename to Sources/AccessibleLoading/Haptic/AccessibilityHapticPatternFactory.swift index a8f0a06..c699c69 100644 --- a/Sources/AccessibleLoading/AccessibilityFeedbackEngine.swift +++ b/Sources/AccessibleLoading/Haptic/AccessibilityHapticPatternFactory.swift @@ -3,24 +3,24 @@ import Foundation import UIKit @available(iOS 13.0, *) -final class AccessibilityFeedbackEngine { +final class AccessibilityHapticPatternFactory { // MARK: - Internal Methods - func start(duration: TimeInterval) -> CHHapticPattern? { - guard UIAccessibility.isVoiceOverRunning else { + func makeOnStartActivityPattern(duration: TimeInterval) -> CHHapticPattern? { + guard isVoiceOverRunning else { return nil } - let events = makeEvents(duration: duration) + let events = makeActivityEvents(duration: duration) return try? CHHapticPattern(events: events, parameterCurves: []) } - func stop() -> CHHapticPattern? { - guard UIAccessibility.isVoiceOverRunning else { + func makeOnStopActivityPattern() -> CHHapticPattern? { + guard isVoiceOverRunning else { return nil } - let event = makeStopEvent() - return try? CHHapticPattern(events: event, parameterCurves: []) + let events = makeActivityStopEvent() + return try? CHHapticPattern(events: events, parameterCurves: []) } // MARK: - Private Types @@ -38,27 +38,32 @@ final class AccessibilityFeedbackEngine { static let delayToFinalEvent: TimeInterval = 0.2 } + // MARK: - Private Properties + + private var isVoiceOverRunning: Bool { UIAccessibility.isVoiceOverRunning } + // MARK: - Private Methods - private func makeEvents(duration: TimeInterval) -> [CHHapticEvent] { + private func makeActivityEvents(duration: TimeInterval) -> [CHHapticEvent] { let interval = 1 / TimeInterval(Constants.eventsPerSecond) - return (0.. CHHapticEvent { - return makeEvent(with: Constants.standardEvent, time: time) + private func makeActivityLoadingEvent(time: TimeInterval) -> CHHapticEvent { + return makeActivityEvent(with: Constants.standardEvent, time: time) } - private func makeStopEvent() -> [CHHapticEvent] { - let beforeFinalEvent = makeEvent(with: Constants.penultimateEvent, time: 0) - let finalEvent = makeEvent(with: Constants.finalEvent, time: Constants.delayToFinalEvent) + private func makeActivityStopEvent() -> [CHHapticEvent] { + let beforeFinalEvent = makeActivityEvent(with: Constants.penultimateEvent, time: 0) + let finalEvent = makeActivityEvent(with: Constants.finalEvent, time: Constants.delayToFinalEvent) return [beforeFinalEvent, finalEvent] } - private func makeEvent(with event: Event, time: TimeInterval) -> CHHapticEvent { + private func makeActivityEvent(with event: Event, time: TimeInterval) -> CHHapticEvent { let intensityParameter = CHHapticEventParameter(parameterID: .hapticIntensity, value: event.intensity) let sharpnessParameter = CHHapticEventParameter(parameterID: .hapticSharpness, value: event.sharpness) return CHHapticEvent( diff --git a/Sources/AccessibleLoading/Haptic/Helpers/DelayedWorkItemExecutor.swift b/Sources/AccessibleLoading/Haptic/Helpers/DelayedWorkItemExecutor.swift new file mode 100644 index 0000000..812f115 --- /dev/null +++ b/Sources/AccessibleLoading/Haptic/Helpers/DelayedWorkItemExecutor.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine + +final class DelayedWorkItemExecutor: Cancellable { + + // MARK: - Internal Init + + init(delay: TimeInterval, queue: DispatchQueue = .main, _ block: @escaping () -> Void) { + self.workItem = DispatchWorkItem(block: block) + queue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + + deinit { + cancel() + } + + // MARK: - Internal Methods + + func cancel() { + workItem.cancel() + } + + // MARK: - Private Properties + + private let workItem: DispatchWorkItem + +}