一个权限请求,iOS 要写 40 行,Android 要写 50 行,还要处理”不再询问”的各种边界情况。我们用 Claude Code 一次性生成双端统一封装,以后新项目直接复用。
1、你写过多少遍权限代码?
做音视频开发,每个 App 都逃不过:
// iOS - 第 N 次写这段
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted { /* 打开相机 */ }
else { /* 提示去设置 */ }
}
}
// Android - 第 N 次写这段
val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) { /* 打开相机 */ }
else { /* 判断是否勾了"不再询问" */ }
}
痛点总结:
- API 差异大——iOS 用闭包回调,Android 用 ActivityResultLauncher
- 边界情况多——「拒绝」「不再询问」「系统设置中关闭」三种状态,各自处理
- 权限状态要同步——进入页面要检查当前状态,从设置回来要重新检查
- 每个项目都重写——样板代码复制粘贴,容易遗漏
2、 技术背景:双端权限模型对比
┌─────────────────────────────────────────────────────────┐
│ │
│ iOS 权限模型 Android 权限模型 │
│ ──────────── ──────────────── │
│ 请求 → 用户选择 请求 → 用户选择 │
│ │ │ │
│ ├─ 允许 → 正常使用 ├─ 允许 → 正常使用 │
│ ├─ 拒绝 → 引导去设置 ├─ 拒绝 → 可再次请求 │
│ └─ (无"不再询问"概念) └─ 拒绝+不再询问 → │
│ 只能引导去设置 │
│ │
│ iOS 检查方式: Android 检查方式: │
│ AVCaptureDevice ContextCompat │
│ .authorizationStatus .checkSelfPermission │
│ │
└─────────────────────────────────────────────────────────┘
| 对比维度 | iOS | Android |
|---|---|---|
| 权限声明 | Info.plist | AndroidManifest.xml |
| 请求方式 | AVCaptureDevice.requestAccess | ActivityResultContracts.RequestPermission |
| 状态检查 | .authorizationStatus | ContextCompat.checkSelfPermission |
| 「不再询问」 | 无(iOS 无此概念) | shouldShowRequestPermissionRationale |
| 引导去设置 | UIApplicationOpenSettingsURLString | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) |
3、Claude Code 介入时刻
3.1、 我这样跟 Claude Code 说
帮我写一个 iOS 和 Android 双端统一的摄像头权限管理工具类。
要求:
1. iOS (Swift):封装 AVCaptureDevice 权限请求
- 检查当前权限状态
- 请求权限
- 如果被拒绝,引导用户去系统设置
- 从设置返回后自动重新检查
2. Android (Kotlin):封装 Manifest.permission.CAMERA 权限
- 检查当前权限状态
- 请求权限
- 处理"不再询问"的情况
- 引导用户去应用设置页面
3. 设计要求:
- 双端接口尽量统一(相同的调用语义)
- 支持 Compose / SwiftUI 的回调方式
- 完整的错误处理和日志
- 可以作为项目的基础工具类直接复用
4. 输出文件:
- ios/CameraPermissionManager.swift
- android/CameraPermissionManager.kt
3.2、 Claude Code 生成了什么(核心代码)
3.2.1、iOS 端 — CameraPermissionManager.swift
import AVFoundation
import UIKit
import Combine
// MARK: - 权限状态枚举(双端统一命名)
enum CameraPermissionStatus {
case authorized // 已授权,可以使用
case denied // 已拒绝(可再次请求)
case restricted // 受限制(家长控制等,仅 iOS)
case notDetermined // 尚未请求过
case permanentlyDenied // 已拒绝且无法再次请求(仅 Android,iOS 保留兼容)
}
// MARK: - 权限管理器
finalclass CameraPermissionManager: ObservableObject {
staticlet shared = CameraPermissionManager()
@Publishedvar status: CameraPermissionStatus = .notDetermined
privateinit() {
refreshStatus()
}
// MARK: - 检查当前权限状态
func refreshStatus() {
let avStatus = AVCaptureDevice.authorizationStatus(for: .video)
DispatchQueue.main.async {
self.status = self.mapStatus(avStatus)
}
}
// MARK: - 请求权限
func requestPermission() async -> CameraPermissionStatus {
let currentStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch currentStatus {
case .authorized:
return .authorized
case .notDetermined:
// 首次请求——系统弹窗
let granted = await AVCaptureDevice.requestAccess(for: .video)
let newStatus: CameraPermissionStatus = granted ? .authorized : .denied
await MainActor.run { self.status = newStatus }
return newStatus
case .denied:
// 已拒绝——引导去设置
await MainActor.run { self.status = .denied }
openSettings()
return .denied
case .restricted:
await MainActor.run { self.status = .restricted }
return .restricted
@unknown default:
return .notDetermined
}
}
// MARK: - 引导用户去系统设置
func openSettings() {
guardlet url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else {
print("[CameraPermission] 无法打开设置")
return
}
UIApplication.shared.open(url)
}
// MARK: - 监听从设置返回(App 回到前台时重新检查)
func startObservingForeground() {
NotificationCenter.default.addObserver(
self,
selector: #selector(willEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
@objcprivatefunc willEnterForeground() {
refreshStatus()
print("[CameraPermission] 从后台返回,当前状态: \(status)")
}
// MARK: - 状态映射
privatefunc mapStatus(_ avStatus: AVAuthorizationStatus) -> CameraPermissionStatus {
switch avStatus {
case .authorized: return .authorized
case .denied: return .denied
case .restricted: return .restricted
case .notDetermined: return .notDetermined
@unknown default: return .notDetermined
}
}
}
3.2.2、Android 端 — CameraPermissionManager.kt
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
// MARK: - 权限状态枚举(与 iOS 对齐)
enumclass CameraPermissionStatus {
AUTHORIZED, // 已授权
DENIED, // 已拒绝(可再次请求)
RESTRICTED, // 受限制(仅 iOS,Android 保留兼容)
NOT_DETERMINED, // 尚未请求过
PERMANENTLY_DENIED // 用户勾选「不再询问」
}
// MARK: - 权限管理器
class CameraPermissionManager private constructor(
privateval activity: ComponentActivity
) : DefaultLifecycleObserver {
companionobject {
privateconstval TAG = "CameraPermission"
@Volatile
privatevar instance: CameraPermissionManager? = null
fun init(activity: ComponentActivity): CameraPermissionManager {
return instance ?: synchronized(this) {
instance ?: CameraPermissionManager(activity).also {
instance = it
activity.lifecycle.addObserver(it)
}
}
}
fun getInstance(): CameraPermissionManager {
return instance ?: throw IllegalStateException(
"CameraPermissionManager 未初始化,请先在 Activity.onCreate 中调用 init()"
)
}
}
// 权限状态流(Compose 可收集)
privateval _status = MutableStateFlow(CameraPermissionStatus.NOT_DETERMINED)
val status: StateFlow<CameraPermissionStatus> = _status.asStateFlow()
// 权限请求 Launcher
privatevar permissionLauncher: ActivityResultLauncher<String>? = null
// 权限请求回调
privatevar onResult: ((CameraPermissionStatus) -> Unit)? = null
/**
* 在 Activity.onCreate 中调用,注册权限 Launcher
*/
fun register(activity: ComponentActivity) {
permissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
val newStatus = if (granted) {
CameraPermissionStatus.AUTHORIZED
} else {
// 判断用户是否勾选了「不再询问」
if (activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
CameraPermissionStatus.DENIED // 仅拒绝,可再次请求
} else {
CameraPermissionStatus.PERMANENTLY_DENIED // 勾了不再询问
}
}
_status.value = newStatus
onResult?.invoke(newStatus)
android.util.Log.d(TAG, "权限请求结果: $newStatus")
}
}
/**
* 检查当前权限状态
*/
fun refreshStatus(): CameraPermissionStatus {
val currentStatus = when {
ContextCompat.checkSelfPermission(
activity, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> CameraPermissionStatus.AUTHORIZED
// 从未请求过
activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
CameraPermissionStatus.NOT_DETERMINED
}
else -> {
// 已拒绝 + 不再询问 → 永久拒绝
// 已拒绝(首次)→ 仍可请求
// 这里简化判断:如果没有授权且 shouldShowRationale 返回 false,
// 说明要么是首次(NOT_DETERMINED),要么是永久拒绝
CameraPermissionStatus.PERMANENTLY_DENIED
}
}
_status.value = currentStatus
return currentStatus
}
/**
* 请求权限(挂起函数,供协程调用)
*/
suspendfun requestPermission(): CameraPermissionStatus {
val launcher = permissionLauncher
?: throw IllegalStateException("请先调用 register() 注册权限 Launcher")
return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
onResult = { result ->
continuation.resume(result) {}
}
launcher.launch(Manifest.permission.CAMERA)
}
}
/**
* 引导用户去应用设置页面
*/
fun openAppSettings() {
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${activity.packageName}")
).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (intent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(intent)
} else {
android.util.Log.e(TAG, "无法打开应用设置页面")
}
}
overridefun onStart(owner: LifecycleOwner) {
super.onStart(owner)
// App 从后台返回时自动刷新权限状态
refreshStatus()
}
overridefun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
owner.lifecycle.removeObserver(this)
}
}
3.3、 我审查时改了哪里
| AI 输出的问题 | 我的修正 | 为什么 |
|---|---|---|
Android 首次请求时 shouldShowRationale 返回 false 被误判为 PERMANENTLY_DENIED | 加了一层判断:先检查是否 NOT_DETERMINED | 首次安装时 shouldShowRationale 也返回 false |
| iOS 没有监听从设置页面返回 | 加了 willEnterForeground Notification 监听 | 用户从设置改了权限回来需要自动刷新 |
Android Manager 的 instance 在 Activity 重建时会丢失 | 加了 synchronized + @Volatile + 生命周期观察 | 配置变更(旋转屏幕)时保持单例 |
4、使用示例
4.1、iOS — SwiftUI
struct CameraView: View {
@StateObjectprivatevar permissionManager = CameraPermissionManager.shared
var body: some View {
Group {
switch permissionManager.status {
case .authorized:
CameraPreviewView()
case .notDetermined:
Button("开启摄像头") {
Task {
_ = await permissionManager.requestPermission()
}
}
case .denied:
VStack {
Text("摄像头权限被拒绝")
Button("去设置") {
permissionManager.openSettings()
}
}
default:
Text("摄像头不可用")
}
}
.onAppear {
permissionManager.refreshStatus()
permissionManager.startObservingForeground()
}
}
}
4.2、Android — Jetpack Compose
@Composable
fun CameraScreen(activity: ComponentActivity) {
val manager = remember { CameraPermissionManager.getInstance() }
val status by manager.status.collectAsState()
DisposableEffect(activity) {
manager.register(activity)
onDispose { }
}
when (status) {
CameraPermissionStatus.AUTHORIZED -> {
CameraPreview()
}
CameraPermissionStatus.NOT_DETERMINED -> {
Button(onClick = {
// 在协程中请求权限
lifecycleScope.launch {
manager.requestPermission()
}
}) {
Text("开启摄像头")
}
}
CameraPermissionStatus.DENIED -> {
Column {
Text("摄像头权限被拒绝")
Button(onClick = { manager.requestPermission() }) {
Text("再次请求")
}
}
}
CameraPermissionStatus.PERMANENTLY_DENIED -> {
Column {
Text("请在设置中开启摄像头权限")
Button(onClick = { manager.openAppSettings() }) {
Text("去设置")
}
}
}
else -> {
Text("摄像头不可用")
}
}
}
5、效果验证
| 指标 | 传统方式(手写) | Claude Code 生成 |
|---|---|---|
| 开发耗时 | ~2 小时(iOS 40min + Android 60min + 联调 20min) | ~30 分钟(生成 5min + 审查修改 15min + 测试 10min) |
| 代码行数 | iOS 80 行 + Android 100 行 | iOS 80 行 + Android 110 行(更健壮) |
| 边界情况覆盖 | 2/5(容易遗漏) | 5/5(AI 枚举了所有状态) |
| 可复用性 | “下个项目再复制吧” | 直接复用 |
Claude Code 帮我省了 75% 的时间。最有价值的是它自动处理了「不再询问」、「从设置返回」等容易遗漏的边界情况。
学习和提升音视频开发技术,欢迎你加入我们的知识星球

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。