chore: drop iOS 18–25 / non-iPhone support, require iOS 26

Two related cleanups the user asked for in one shot:

1. iPhone-only is now enforced at every target — Mac Catalyst and
   visionOS were never configured in `project.yml`, but the
   `OSGKeyboardShared` framework and the two test bundles were
   still defaulting to `TARGETED_DEVICE_FAMILY = "1,2"` (iPhone +
   iPad). All four targets now explicitly set `"1"`. SDK is
   `iphoneos` for everyone, no `xros` / `macosx`.

2. Deployment target bumped from iOS 18.0 to iOS 26.0 across the
   board (`project.yml` + the ext's per-target setting). With
   iOS 26 as the floor, the iOS 18–25 SFSpeechRecognizer path
   became dead code and several `#available` checks became
   always-true. Removed:

   - `AppleSpeechASR` (the entire SFSpeechRecognizer-based ASR
     backend) and the `#available(iOS 26.0, *)` factory branch.
     `ASRServiceFactory.make()` now returns `SpeechAnalyzerASR()`
     directly. SpeechAnalyzer is always fully on-device, which
     also made the `requiresOnDevice` flag meaningless.
   - `requiresOnDevice` from the `ASRService.transcribe` protocol
     signature, from `ProviderConfig`, `AppGroupStore`,
     `KeyboardState`, `AppGroupPersistor`, and the ext's
     `KeyboardViewController` (`state.requiresOnDevice`,
     `state.setRequiresOnDevice`, `persistRequiresOnDevice`).
   - `#available(iOS 17.0, *)` branch in
     `PreviewASRController.requestMicrophonePermission` and the
     ext's `PermissionManager.requestMicPermission` — both now
     just call the iOS 17+ `AVAudioApplication` API directly.
   - The `else` (iOS 18–25) branch in `SettingsView.asrEngineRow`
     — the on-device-only toggle is gone, the row is a static
     "SpeechAnalyzer active" badge. Same for the `else` branch
     in `EnginePickerSection.localSubtitle`.
   - `makeMicAuthHandler` (the iOS < 17 mic permission callback
     wrapper) from `PreviewASRController`.

   No `#available` / `@available` checks remain in the codebase
   except for the SpeechAnalyzer class itself (now unnecessary
   too, but kept for clarity — `AVAudioApplication` and
   `SpeechAnalyzer` are both iOS 17+ / iOS 26+ respectively,
   and the deployment target of 26 makes the explicit
   `@available` redundant; I left the SpeechAnalyzer class
   un-`@available` and removed the `@available(iOS 26.0, *)`
   decoration since it's no longer needed).

   The Keyboard ext's existing ASRService usage
   (`asr.transcribe(stream:locale:)`) is unchanged at the
   call-site level — just the third argument is gone.

3. Updated `info.plist` UISupportedInterfaceOrientations is
   already `[UIInterfaceOrientationPortrait]` only, which is
   correct for an iPhone-only app; no change needed.

Build: BUILD SUCCEEDED.
Tests: 22/22 pass.
Verified: `TARGETED_DEVICE_FAMILY = 1` on all four targets,
`SDKROOT = iphoneos` on all four.

🤖 Generated with Claude Code
This commit is contained in:
Rocky
2026-06-18 20:24:26 +08:00
parent a803a27a88
commit 81581f0e5f
11 changed files with 85 additions and 291 deletions
@@ -63,14 +63,10 @@ public final class KeyboardState: ObservableObject {
@Published public var localeId: String = "auto"
@Published public var lastTranscript: String = ""
/// `true` if the active ASR session is running on-device for the
/// current locale. `false` means the request fell back to the
/// network (e.g. ja-JP on a device that doesn't ship on-device
/// ASR for Japanese). Updated once per recording by the ASR
/// pipeline before any `.partial` is emitted.
/// current locale. With iOS 26's `SpeechAnalyzer` this is always
/// `true` kept on the state object because the UI's status
/// badge still wants a single source of truth to read from.
@Published public var onDeviceSupported: Bool = false
/// When `true`, `SFSpeechRecognizer` is forced to on-device mode.
/// Ignored on iOS 26+ where `SpeechAnalyzer` is always on-device.
@Published public var requiresOnDevice: Bool = false
/// "local" ASR only, no LLM. "cloud" ASR + optional LLM polish.
@Published public var engineMode: String = "cloud"
@@ -84,7 +80,6 @@ public final class KeyboardState: ObservableObject {
public var openSettings: () -> Void = {}
public var setMode: (InputMode) -> Void = { _ in }
public var setLocale: (String) -> Void = { _ in }
public var setRequiresOnDevice: (Bool) -> Void = { _ in }
public var setEngineMode: (String) -> Void = { _ in }
public var insertNewline: () -> Void = {}
public var insertSpace: () -> Void = {}