81581f0e5f
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
62 lines
2.3 KiB
Swift
62 lines
2.3 KiB
Swift
// PermissionManager.swift
|
|
// OSGKeyboard · Keyboard Extension
|
|
//
|
|
// Extracted from KeyboardViewController so the view controller doesn't
|
|
// need to know about AVAudioApplication vs AVAudioSession branching
|
|
// or SFSpeechRecognizer.requestAuthorization callback bridging.
|
|
//
|
|
// Contract:
|
|
// • `requestMicPermission()` returns true if the user has authorised
|
|
// or *just* authorised; false otherwise. Idempotent within a
|
|
// process — the second call will not prompt again if the user has
|
|
// already answered.
|
|
// • `requestSpeechPermission()` mirrors the same shape but for
|
|
// SFSpeechRecognizer.
|
|
|
|
import Foundation
|
|
import AVFoundation
|
|
import Speech
|
|
|
|
@MainActor
|
|
public final class PermissionManager: @unchecked Sendable {
|
|
|
|
public init() {}
|
|
|
|
private var didRequestMicOnce: Bool = false
|
|
|
|
/// Request microphone access. Returns true if granted (already or
|
|
/// after this call). Uses the iOS 17+ `AVAudioApplication` API.
|
|
public func requestMicPermission() async -> Bool {
|
|
switch AVAudioApplication.shared.recordPermission {
|
|
case .granted: return true
|
|
case .denied: return false
|
|
case .undetermined:
|
|
if !didRequestMicOnce {
|
|
didRequestMicOnce = true
|
|
return await AVAudioApplication.requestRecordPermission()
|
|
}
|
|
return false
|
|
@unknown default: return false
|
|
}
|
|
}
|
|
|
|
/// Request Speech Recognition permission. Returns true if granted
|
|
/// (already or after this call). The `SFSpeechRecognizer` plist
|
|
/// key + this call are still required even on iOS 26 — the
|
|
/// `SpeechAnalyzer` API does not expose an explicit request
|
|
/// method of its own and the framework checks the same TCC
|
|
/// entry on first use.
|
|
public func requestSpeechPermission() async -> Bool {
|
|
await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
|
|
SFSpeechRecognizer.requestAuthorization { status in
|
|
switch status {
|
|
case .authorized: cont.resume(returning: true)
|
|
case .denied, .restricted, .notDetermined:
|
|
cont.resume(returning: false)
|
|
@unknown default:
|
|
cont.resume(returning: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |