feat(keyboard): improve typing, voice flow, and polish reliability
Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user