40760aa884
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调 - desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导) - android:Compose 主 App + IME(键盘内录音直传) - ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写) - design/design-pipeline:设计系统 + token 导出 iOS/Android 主题 - doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
3.4 KiB
Swift
78 lines
3.4 KiB
Swift
// 1A spike 核心:键盘扩展内尝试 AVAudioEngine 录音,结果直接显示在键盘上。
|
||
// 判定标准:
|
||
// ✅ engine.start() 成功且 1 秒内收到音频帧 → 键盘内录音可行(主路径成立)
|
||
// ❌ setActive/start 抛错或 0 帧 → 不可行,走 deep link 降级方案(14D)
|
||
import AVFoundation
|
||
import UIKit
|
||
|
||
class KeyboardViewController: UIInputViewController {
|
||
private let resultLabel = UILabel()
|
||
private let engine = AVAudioEngine()
|
||
private var frames: UInt32 = 0
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
view.backgroundColor = .systemGray5
|
||
|
||
let stack = UIStackView()
|
||
stack.axis = .vertical
|
||
stack.spacing = 10
|
||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||
view.addSubview(stack)
|
||
NSLayoutConstraint.activate([
|
||
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||
stack.topAnchor.constraint(equalTo: view.topAnchor, constant: 16),
|
||
stack.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -32),
|
||
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 220),
|
||
])
|
||
|
||
let info = UILabel()
|
||
info.text = "完全访问:\(hasFullAccess ? "已开启" : "未开启(请先开启)")"
|
||
info.font = .systemFont(ofSize: 14)
|
||
stack.addArrangedSubview(info)
|
||
|
||
let btn = UIButton(type: .system)
|
||
btn.setTitle("测试键盘内录音(1 秒)", for: .normal)
|
||
btn.titleLabel?.font = .boldSystemFont(ofSize: 17)
|
||
btn.addTarget(self, action: #selector(testMic), for: .touchUpInside)
|
||
stack.addArrangedSubview(btn)
|
||
|
||
resultLabel.text = "结果:未测试"
|
||
resultLabel.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
|
||
resultLabel.numberOfLines = 0
|
||
stack.addArrangedSubview(resultLabel)
|
||
|
||
let next = UIButton(type: .system)
|
||
next.setTitle("🌐 切回系统键盘", for: .normal)
|
||
next.addTarget(self, action: #selector(handleInputModeList(from:with:)), for: .allTouchEvents)
|
||
stack.addArrangedSubview(next)
|
||
}
|
||
|
||
@objc private func testMic() {
|
||
frames = 0
|
||
resultLabel.text = "结果:测试中…"
|
||
// 键盘扩展无法弹权限框;权限继承容器 App / 系统设置中的麦克风授权
|
||
do {
|
||
let session = AVAudioSession.sharedInstance()
|
||
try session.setCategory(.playAndRecord, mode: .measurement, options: [.mixWithOthers])
|
||
try session.setActive(true)
|
||
engine.inputNode.removeTap(onBus: 0)
|
||
engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buf, _ in
|
||
self?.frames += buf.frameLength
|
||
}
|
||
try engine.start()
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||
guard let self else { return }
|
||
self.engine.stop()
|
||
self.engine.inputNode.removeTap(onBus: 0)
|
||
let permission = AVAudioApplication.shared.recordPermission
|
||
self.resultLabel.text = self.frames > 0
|
||
? "结果:✅ 收到 \(self.frames) 帧 — 键盘内录音可行"
|
||
: "结果:❌ 0 帧(权限态:\(permission.rawValue))— 需走降级方案"
|
||
}
|
||
} catch {
|
||
resultLabel.text = "结果:❌ \(error.localizedDescription) — 需走降级方案"
|
||
}
|
||
}
|
||
}
|