在视频会议、直播演讲或录制课程时,提词器已经成为内容创作者的刚需工具。传统提词方案要么需要额外硬件设备,要么占用主屏幕空间影响操作体验。iOS 14引入的AVPictureInPictureController为这个问题提供了优雅的解决方案——将提词器以悬浮窗形式呈现,实现真正的多任务并行处理。
这个看似简单的功能背后,隐藏着许多技术细节和实现陷阱。本文将带你从零构建一个支持自定义样式、后台保活和手势交互的专业级悬浮提词器,并分享通过App Store审核的实战经验。无论你是想开发提词器、悬浮计时器还是实时数据看板,这些技术方案都能直接复用。
一个完整的悬浮提词器需要解决三个核心问题:内容渲染层如何叠加在视频层之上、系统控件的自定义处理,以及后台持续运行的保活机制。AVPictureInPictureController虽然主要面向视频播放设计,但通过巧妙的"视频载体+自定义视图"方案,完全可以实现纯文本的悬浮展示。
技术选型对比表:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原生AVPictureInPictureController | 系统级支持,性能稳定 | 需要视频载体 | 提词器、悬浮时钟 |
| UIWindow方案 | 完全自定义 | 无法突破应用沙盒 | 应用内悬浮窗 |
| WebRTC屏幕共享 | 跨应用可见 | 需要用户授权 | 直播辅助工具 |
实现的基本原理是创建一个不可见的MP4视频作为载体(可以是纯色背景或极低码率视频),在其上叠加自定义的UITextView用于显示台词文本。关键代码结构如下:
swift复制class TeleprompterViewController: UIViewController {
private let pipController: AVPictureInPictureController
private let playerLayer = AVPlayerLayer()
private let textView = UITextView()
override func viewDidLoad() {
setupInvisibleVideoPlayer()
configurePIPController()
addCustomTextOverlay()
}
func setupInvisibleVideoPlayer() {
let videoURL = Bundle.main.url(forResource: "transparent", withExtension: "mp4")!
let player = AVPlayer(url: videoURL)
playerLayer.player = player
player.play()
}
}
默认的画中画窗口会显示播放进度条和控制按钮,这对提词器场景完全是干扰。通过私有API可以隐藏这些元素,但需要注意审核风险。更稳妥的做法是利用requiresLinearPlayback属性:
swift复制pipController.setValue(1, forKey: "requiresLinearPlayback")
添加自定义视图的关键在于时机选择——必须在画中画窗口创建完成但尚未显示时插入。最佳实践是在pictureInPictureControllerWillStartPictureInPicture回调中操作:
swift复制func pictureInPictureControllerWillStartPictureInPicture(_ controller: AVPictureInPictureController) {
guard let pipWindow = UIApplication.shared.windows.first(where: { $0.rootViewController == nil }) else { return }
textView.translatesAutoresizingMaskIntoConstraints = false
pipWindow.addSubview(textView)
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: pipWindow.topAnchor),
textView.leadingAnchor.constraint(equalTo: pipWindow.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: pipWindow.trailingAnchor),
textView.bottomAnchor.constraint(equalTo: pipWindow.bottomAnchor)
])
}
常见问题排查清单:
提词器需要长时间稳定运行,但iOS的后台限制会随时中断进程。通过"无声音频+画中画"的组合拳可以实现伪后台保活:
Audio, AirPlay, and Picture in Picture后台模式权限swift复制private func setupSilentAudioTrack() {
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(.playback, options: .mixWithOthers)
let silentAudioURL = Bundle.main.url(forResource: "silence", withExtension: "mp3")!
let audioPlayer = AVPlayer(url: silentAudioURL)
audioPlayer.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: audioPlayer.currentItem,
queue: nil) { _ in
audioPlayer.seek(to: .zero)
audioPlayer.play()
}
audioPlayer.play()
}
性能优化指标参考:
| 优化点 | 前 | 后 | 测量工具 |
|---|---|---|---|
| 内存占用 | 45MB | 28MB | Xcode Memory Gauge |
| CPU使用率 | 18% | 9% | Instruments |
| 启动时间 | 1.2s | 0.7s | XCTest metrics |
苹果对滥用后台权限的应用审核严格,但合理的功能设计可以通过审核。建议采用"主次功能"策略:
审核回复模板:
code复制尊敬的审核团队:
我们的应用主要功能是视频学习工具,画中画模式允许用户在浏览其他内容时继续观看教学视频。提词器是面向讲师用户的辅助工具,同样基于系统提供的画中画API实现。所有后台行为均符合Apple开发者指南第3.3.2节规范。
实际项目中,我们发现在提词器界面添加一个"如何使用"的视频教程按钮,能显著提高审核通过率。这个视频本身就可以作为画中画功能合法性的证明。
基础功能稳定后,可以考虑添加这些提升用户体验的功能:
文字滚动控制方案对比:
| 方案 | 精度 | 功耗 | 实现难度 |
|---|---|---|---|
| CADisplayLink | 高 | 中 | 低 |
| GCD定时器 | 中 | 低 | 中 |
| DispatchSourceTimer | 高 | 低 | 高 |
推荐使用CADisplayLink实现平滑滚动:
swift复制private func setupScrolling() {
let displayLink = CADisplayLink(target: self, selector: #selector(updateScroll))
displayLink.add(to: .main, forMode: .common)
}
@objc private func updateScroll() {
let speed: CGFloat = 0.5 // 像素/帧
textView.contentOffset.y += speed
if textView.contentOffset.y > textView.contentSize.height {
textView.contentOffset.y = -textView.bounds.height
}
}
手势交互方面,建议实现这些操作:
在iPhone 13 Pro上测试,即使同时开启画中画提词器、视频录制和后台音乐播放,整个系统仍能保持流畅运行。这证明经过优化的方案完全具备生产环境可用性。