31f5937a7f
Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
106 lines
3.1 KiB
Swift
106 lines
3.1 KiB
Swift
// KeyboardHapticFeedback.swift
|
|
// OSGKeyboard · Keyboard Extension
|
|
//
|
|
// Role-based typing haptics (no screen-position mapping). Intensity comes
|
|
// from Settings → General → Haptics (off / light / strong).
|
|
|
|
import UIKit
|
|
import OSGKeyboardShared
|
|
|
|
/// Key roles drive distinct Taptic styles — closer to a real keyboard than
|
|
/// a single buzz for every tap.
|
|
enum KeyboardHapticKeyRole {
|
|
/// Letters, digits, punctuation.
|
|
case character
|
|
/// Shift, 123 / ABC / #+=.
|
|
case modifier
|
|
/// Space, return / send.
|
|
case action
|
|
/// Delete (including hold-to-repeat ticks).
|
|
case delete
|
|
}
|
|
|
|
@MainActor
|
|
enum KeyboardHapticFeedback {
|
|
private static let soft = UIImpactFeedbackGenerator(style: .soft)
|
|
private static let light = UIImpactFeedbackGenerator(style: .light)
|
|
private static let medium = UIImpactFeedbackGenerator(style: .medium)
|
|
private static let heavy = UIImpactFeedbackGenerator(style: .heavy)
|
|
private static let rigid = UIImpactFeedbackGenerator(style: .rigid)
|
|
|
|
/// Warm the generators so the first keypress is not soft/late.
|
|
static func prepare() {
|
|
soft.prepare()
|
|
light.prepare()
|
|
medium.prepare()
|
|
heavy.prepare()
|
|
rigid.prepare()
|
|
}
|
|
|
|
static func play(role: KeyboardHapticKeyRole, intensity: KeyboardHapticIntensity) {
|
|
guard intensity != .off else { return }
|
|
|
|
switch intensity {
|
|
case .off:
|
|
return
|
|
case .light:
|
|
playLight(role: role)
|
|
case .strong:
|
|
playStrong(role: role)
|
|
}
|
|
}
|
|
|
|
// Soft / light — near stock keyboard feedback.
|
|
private static func playLight(role: KeyboardHapticKeyRole) {
|
|
switch role {
|
|
case .character:
|
|
soft.impactOccurred(intensity: 0.55)
|
|
soft.prepare()
|
|
case .modifier:
|
|
light.impactOccurred(intensity: 0.65)
|
|
light.prepare()
|
|
case .action:
|
|
light.impactOccurred(intensity: 0.8)
|
|
light.prepare()
|
|
case .delete:
|
|
medium.impactOccurred(intensity: 0.45)
|
|
medium.prepare()
|
|
}
|
|
}
|
|
|
|
// Heavier / sharper — more mechanical-keyboard feel.
|
|
private static func playStrong(role: KeyboardHapticKeyRole) {
|
|
switch role {
|
|
case .character:
|
|
medium.impactOccurred(intensity: 0.75)
|
|
medium.prepare()
|
|
case .modifier:
|
|
medium.impactOccurred(intensity: 0.95)
|
|
medium.prepare()
|
|
case .action:
|
|
heavy.impactOccurred(intensity: 0.9)
|
|
heavy.prepare()
|
|
case .delete:
|
|
rigid.impactOccurred(intensity: 1.0)
|
|
rigid.prepare()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sound + haptic for a typing-grid key press (pressed-down timing).
|
|
@MainActor
|
|
enum TypingKeyFeedback {
|
|
static func play(
|
|
role: KeyboardHapticKeyRole,
|
|
intensity: KeyboardHapticIntensity,
|
|
isDelete: Bool = false
|
|
) {
|
|
if isDelete {
|
|
KeyboardSoundFeedback.deleteClick()
|
|
} else {
|
|
KeyboardSoundFeedback.keyClick()
|
|
}
|
|
KeyboardHapticFeedback.play(role: role, intensity: intensity)
|
|
}
|
|
}
|