##更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!
需求分析:
最近项目需求需要麦克风录音权限,因为整体上的UI界面是前端wkwebview搭建的,实现功能逻辑是由iOS实现,没有用原生!然后就出现了需要麦克风录音机跟H5交互的功能模块!
查了资料都文章说iOS对h5交互麦克风录音不友好
现在具体工作流程步骤如下:
- 首先创建了一个wkwebview
//加载webview视图
override func loadView() {
let preference = WKPreferences()
preference.minimumFontSize = 0
preference.javaScriptEnabled = true
preference.javaScriptCanOpenWindowsAutomatically = true
preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
debugPrint("这里已经进来了")
// swift 提供给 h5 调用方法
let userContentController = WKUserContentController()
userContentController.add(self, name: "callAudio") //调起iOS音频权限
userContentController.add(self, name: "recorderStart") //开始录音
userContentController.add(self, name: "recorderStop") //停止录音
let conf = WKWebViewConfiguration()
conf.userContentController = userContentController
conf.preferences = preference
// let conf = WKWebViewConfiguration();
// conf.userContentController.add(self, name: "callAudio") //调起iOS音频权限
// conf.userContentController.add(self, name: "recorderStart") //开始录音
// conf.userContentController.add(self, name: "recorderStop") //停止录音
webView = WKWebView(frame: CGRect(x:0, y:0, width:SCREEN_WIDTH, height:SCREEN_HEIGHT), configuration: conf)
webView.navigationDelegate = self;
webView.scrollView.isScrollEnabled = false //禁止webview滑动滚动
if #available(iOS 11.0, *) {
webView.scrollView.contentInsetAdjustmentBehavior = .never;
}
view = webView;
}
其中:callAudio、recorderStart、recorderStop是iOS跟webview定义好协议接收的方法
- 重点:加载完成后接收H5调用的协议方法:
// 接受 h5 调用
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let name = message.value(forKey: "name") as? String, let body = message.value(forKey: "body") as? String else { return }
debugPrint("测试链接8888+:\(name)")
if name == "callAudio" {
SystemAuth.authMicrophone { result in
if result{
self.webView.evaluateJavaScript("getPermission('\(result)')", completionHandler: nil)
}else{
DispatchQueue.main.async {
let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
alertView.show()
}
}
}
}
}
SystemAuth.authMicrophone 调用录音麦克风权限返回true跟false
self.webView.evaluateJavaScript("getPermission('(result)')", completionHandler: nil) iOS拦截到方法注入新方法getPermission()携带参数true或者false返回给H5接收
Swift开启iOS的录音权限包括其他照相机权限的代码文件
我整理好在下面的代码了
SystemAuth.Swift
//
// SystemAuth.swift
// Authorization
//
// Created by 柯南 on 2020/9/4.
// Copyright © 2020 LTM. All rights reserved.
//
import UIKit
/// 媒体资料库/Apple Music
import MediaPlayer
import Photos
import UserNotifications
import Contacts
/// Siri权限
import Intents
/// 语音转文字权限
import Speech
/// 日历、提醒事项
import EventKit
/// Face、TouchID
import LocalAuthentication
import HealthKit
import HomeKit
/// 运动与健身权限
import CoreMotion
/// 防止获取无效 计步器
private let cmPedometer = CMPedometer()
typealias AuthClouser = ((Bool)->())
/// 定义私有全局变量,解决在iOS 13 定位权限弹框自动消失的问题
private let locationAuthManager = CLLocationManager()
/**
escaping 逃逸闭包的生命周期:
1,闭包作为参数传递给函数;
2,退出函数;
3,闭包被调用,闭包生命周期结束
即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放
经常使用逃逸闭包的2个场景:
异步调用: 如果需要调度队列中异步调用闭包,比如网络请求成功的回调和失败的回调,这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不确定,上边的例子。
存储: 需要存储闭包作为属性,全局变量或其他类型做稍后使用,例子待补充
*/
public class SystemAuth {
// /**
// 媒体资料库/Apple Music权限
//
// - parameters: action 权限结果闭包
// */
// class func authMediaPlayerService(clouser :@escaping AuthClouser) {
// let authStatus = MPMediaLibrary.authorizationStatus()
// switch authStatus {
// /// 未作出选择
// case .notDetermined:
// MPMediaLibrary.requestAuthorization { (status) in
// if status == .authorized{
// DispatchQueue.main.async {
// clouser(true)
// }
// }else{
// DispatchQueue.main.async {
// clouser(false)
// }
// }
// }
// /// 用户明确拒绝此应用程序的授权,或在设置中禁用该服务。
// case .denied:
// clouser(false)
// /// 该应用程序未被授权使用该服务。由于用户无法改变对该服务的主动限制。此状态,并且个人可能没有拒绝授权。
// case .restricted:
// clouser(false)
// /// 已授权
// case .authorized:
// clouser(true)
// /// 扩展以后可能有的状态,做保护措施
// @unknown default:
// clouser(false)
// }
// }
// /**
// 联网权限
//
// - parameters: action 权限结果闭包
// */
// class func authNetwork(clouser: @escaping AuthClouser) {
//
// let reachabilityManager = NetworkReachabilityManager(host: "www.baidu.com")
// switch reachabilityManager?.status {
// case .reachable(.cellular):
// clouser(true)
// case .reachable(.ethernetOrWiFi):
// clouser(true)
// case .none:
// clouser(false)
// case .notReachable:
// clouser(false)
// // let status = reachabilityManager?.flags
// // switch status {
// // case .none:
// // clouser(false)
// // case .some(.connectionAutomatic):
// // clouser(false)
// // case .some(.connectionOnDemand):
// // clouser(false)
// // case .some(.connectionOnTraffic):
// // clouser(false)
// // case .some(.connectionRequired):
// // clouser(false)
// // case .some(.interventionRequired):
// // clouser(false)
// // case .some(.isDirect):
// // clouser(false)
// // case .some(.isLocalAddress):
// // clouser(false)
// // case .some(.isWWAN):
// // clouser(false)
// // case .some(.reachable):
// // clouser(false)
// // case .some(.transientConnection):
// // clouser(false)
// // case .init(rawValue: 0):
// // clouser(false)
// // case .some(_):
// // clouser(false)
// // }
// case .unknown:
// clouser(false)
// }
// }
/**
相机权限
- parameters: action 权限结果闭包
*/
class func authCamera(clouser: @escaping AuthClouser) {
let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch authStatus {
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { (result) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .denied:
clouser(false)
case .restricted:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
}
/**
相册权限
- parameters: action 权限结果闭包
*/
class func authPhotoLib(clouser: @escaping AuthClouser) {
let authStatus = PHPhotoLibrary.authorizationStatus()
switch authStatus {
case .notDetermined:
PHPhotoLibrary.requestAuthorization { (status) in
if status == .authorized{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .denied:
clouser(false)
case .restricted:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
}
/**
麦克风权限
- parameters: action 权限结果闭包
*/
class func authMicrophone(clouser: @escaping AuthClouser) {
let authStatus = AVAudioSession.sharedInstance().recordPermission
switch authStatus {
case .undetermined:
AVAudioSession.sharedInstance().requestRecordPermission { (result) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .denied:
clouser(false)
case .granted:
clouser(true)
@unknown default:
clouser(false)
}
}
//开启麦克风权限
func openAudioSession() {
let permissionStatus = AVAudioSession.sharedInstance().recordPermission
if permissionStatus == AVAudioSession.RecordPermission.undetermined {
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
//此处可以判断权限状态来做出相应的操作,如改变按钮状态
if granted{
DispatchQueue.main.async {
}
}else{
DispatchQueue.main.async {
let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
alertView.show()
}
}
}
}
}
//是否开启麦克风
func getPermission() -> Bool{
let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
return authStatus != .restricted && authStatus != .denied
}
/**
定位权限
- parameters: action 权限结果闭包(有无权限,是否第一次请求权限)
*/
class func authLocation(clouser: @escaping ((Bool,Bool)->())) {
let authStatus = CLLocationManager.authorizationStatus()
switch authStatus {
case .notDetermined:
//由于IOS8中定位的授权机制改变 需要进行手动授权
locationAuthManager.requestAlwaysAuthorization()
locationAuthManager.requestWhenInUseAuthorization()
let status = CLLocationManager.authorizationStatus()
if status == .authorizedAlways || status == .authorizedWhenInUse {
DispatchQueue.main.async {
clouser(true && CLLocationManager.locationServicesEnabled(), true)
}
}else{
DispatchQueue.main.async {
clouser(false, true)
}
}
case .restricted:
clouser(false, false)
case .denied:
clouser(false, false)
case .authorizedAlways:
clouser(true && CLLocationManager.locationServicesEnabled(), false)
case .authorizedWhenInUse:
clouser(true && CLLocationManager.locationServicesEnabled(), false)
@unknown default:
clouser(false, false)
}
}
// /**
// 推送权限
//
// - parameters: action 权限结果闭包
// */
// class func authNotification(clouser: @escaping AuthClouser){
// UNUserNotificationCenter.current().getNotificationSettings(){ (setttings) in
// switch setttings.authorizationStatus {
// case .notDetermined:
// UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .carPlay, .sound]) { (result, error) in
// if result{
// DispatchQueue.main.async {
// clouser(true)
// }
// }else{
// DispatchQueue.main.async {
// clouser(false)
// }
// }
// }
// case .denied:
// clouser(false)
// case .authorized:
// clouser(true)
// case .provisional:
// clouser(true)
// @unknown default:
// clouser(false)
// }
// }
// }
/**
运动与健身
- parameters: action 权限结果闭包
*/
class func authCMPedometer(clouser: @escaping AuthClouser){
cmPedometer.queryPedometerData(from: Date(), to: Date()) { (pedometerData, error) in
if pedometerData?.numberOfSteps != nil{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
}
/**
通讯录权限
- parameters: action 权限结果闭包
*/
class func authContacts(clouser: @escaping AuthClouser){
let authStatus = CNContactStore.authorizationStatus(for: .contacts)
switch authStatus {
case .notDetermined:
CNContactStore().requestAccess(for: .contacts) { (result, error) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .restricted:
clouser(false)
case .denied:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
}
// /**
// Siri 权限
//
// - parameters: action 权限结果闭包
// */
// class func authSiri(clouser: @escaping AuthClouser){
// let authStatus = INPreferences.siriAuthorizationStatus()
// switch authStatus {
// case .notDetermined:
// INPreferences.requestSiriAuthorization { (status) in
// if status == .authorized{
// DispatchQueue.main.async {
// clouser(true)
// }
// }else{
// DispatchQueue.main.async {
// clouser(false)
// }
// }
// }
// case .restricted:
// clouser(false)
// case .denied:
// clouser(false)
// case .authorized:
// clouser(true)
// @unknown default:
// clouser(false)
// }
// }
/**
语音转文字权限
- parameters: action 权限结果闭包
*/
class func authSpeechRecognition(clouser: @escaping AuthClouser){
if #available(iOS 10.0, *) {
let authStatus = SFSpeechRecognizer.authorizationStatus()
switch authStatus {
case .notDetermined:
SFSpeechRecognizer.requestAuthorization { (status) in
if status == .authorized{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .restricted:
clouser(false)
case .denied:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
} else {
// Fallback on earlier versions
}
}
/**
提醒事项
- parameters: action 权限结果闭包
*/
class func authRreminder(clouser: @escaping AuthClouser){
let authStatus = EKEventStore.authorizationStatus(for: .reminder)
switch authStatus {
case .notDetermined:
EKEventStore().requestAccess(to: .reminder) { (result, error) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .restricted:
clouser(false)
case .denied:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
}
/**
日历
- parameters: action 权限结果闭包
*/
class func authEvent(clouser: @escaping AuthClouser){
let authStatus = EKEventStore.authorizationStatus(for: .event)
switch authStatus {
case .notDetermined:
EKEventStore().requestAccess(to: .event) { (result, error) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
case .restricted:
clouser(false)
case .denied:
clouser(false)
case .authorized:
clouser(true)
@unknown default:
clouser(false)
}
}
/**
FaceID或者TouchID 认证
- parameters: action 权限结果闭包
*/
class func authFaceOrTouchID(clouser: @escaping ((Bool,Error)->())) {
let context = LAContext()
var error: NSError?
let result = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
if result {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "认证") { (success, authError) in
if success{
print("成功")
}else{
print("失败")
}
}
}else{
/**
#define kLAErrorAuthenticationFailed -1
#define kLAErrorUserCancel -2
#define kLAErrorUserFallback -3
#define kLAErrorSystemCancel -4
#define kLAErrorPasscodeNotSet -5
#define kLAErrorTouchIDNotAvailable -6
#define kLAErrorTouchIDNotEnrolled -7
#define kLAErrorTouchIDLockout -8
#define kLAErrorAppCancel -9
#define kLAErrorInvalidContext -10
#define kLAErrorNotInteractive -1004
#define kLAErrorBiometryNotAvailable kLAErrorTouchIDNotAvailable
#define kLAErrorBiometryNotEnrolled kLAErrorTouchIDNotEnrolled
*/
print("不可以使用")
}
}
/**
健康 (写:体能训练、iOS13 听力图 读: 健身记录、体能训练、iOS13 听力图)
- parameters: action 权限结果闭包
*/
class func authHealth(clouser: @escaping AuthClouser){
if HKHealthStore.isHealthDataAvailable(){
let authStatus = HKHealthStore().authorizationStatus(for: .workoutType())
switch authStatus {
case .notDetermined:
if #available(iOS 13.0, *) {
HKHealthStore().requestAuthorization(toShare: [.audiogramSampleType(), .workoutType()], read: [.activitySummaryType(), .workoutType(), .audiogramSampleType()]) { (result, error) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
} else {
if #available(iOS 9.3, *) {
HKHealthStore().requestAuthorization(toShare: [.workoutType()], read: [.activitySummaryType(), .workoutType()]) { (result, error) in
if result{
DispatchQueue.main.async {
clouser(true)
}
}else{
DispatchQueue.main.async {
clouser(false)
}
}
}
} else {
// Fallback on earlier versions
}
}
case .sharingDenied:
clouser(false)
case .sharingAuthorized:
clouser(true)
@unknown default:
clouser(false)
}
}else{
clouser(false)
}
}
/**
家庭、住宅数据
- parameters: action 权限结果闭包
*/
class func authHomeKit(clouser: @escaping AuthClouser) {
if #available(iOS 13.0, *) {
switch HMHomeManager().authorizationStatus {
case .authorized:
clouser(true)
case .determined:
clouser(false)
case .restricted:
clouser(false)
default:
clouser(false)
}
} else {
if (HMHomeManager().primaryHome != nil) {
clouser(true)
}else{
clouser(false)
}
}
}
/**
系统设置
- parameters: urlString 可以为系统,也可以为微信:weixin://、QQ:mqq://
- parameters: action 结果闭包
*/
class func authSystemSetting(urlString :String?, clouser: @escaping AuthClouser) {
var url: URL
if (urlString != nil) && urlString?.count ?? 0 > 0 {
url = URL(string: urlString!)!
}else{
url = URL(string: UIApplication.openSettingsURLString)!
}
if UIApplication.shared.canOpenURL(url){
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:]) { (result) in
if result{
clouser(true)
}else{
clouser(false)
}
}
} else {
// Fallback on earlier versions
}
}else{
clouser(false)
}
}
}
-
重点来了 录制完音频文件后,也是跟前端定义好方法返回回去
让前端利用base64编码发送回去给前端,剩下他就能接收处理
let fileData = try! Data(contentsOf: wavFileURL)
//将图片转为base64编码
let base64 = fileData.base64EncodedString(options: .endLineWithLineFeed).addingPercentEncoding(withAllowedCharacters: .alphanumerics)
UserDefaults.standard.setValue(base64, forKey: "wavFileURL")
4.到这里,你就以为很成功了,能顺利录音-播放-展示到webview了吗?那你就继续入坑吧
原本以为我这里通过base64把录完音的文件发送给前端就没我活了,谁知道录完音老是播放不了
解决方法:
- 由于WKWebView无权限访问本地文件,访问本地文件使用的是file://协议,由于WKWebView的安全机制,会报一些错无法访问到。需要打开webView的file://协议访问权限,设置allowFileAccessFromFileURLs为true。
2.如果出现跨域的报错,也可以通过设置allowUniversalAccessFromFileURLs为true来解决。
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 解决HTML请求跨域
[config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];
WKPreferences *preferences = [[WKPreferences alloc] init];
// 打开web访问本地文件权限
[preferences setValue:@(true) forKey:@"allowFileAccessFromFileURLs"];
config.preferences = preferences;
let preference = WKPreferences()
preference.minimumFontSize = 0
preference.javaScriptEnabled = true
preference.javaScriptCanOpenWindowsAutomatically = true
preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
debugPrint("这里已经进来了")
image.png
更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!
网友评论