##更多方法交流可以家魏鑫:lixiaowu1129,一起探討iOS相關(guān)技術(shù)!
需求分析:
最近項目需求需要麥克風錄音權(quán)限,因為整體上的UI界面是前端wkwebview搭建的,實現(xiàn)功能邏輯是由iOS實現(xiàn),沒有用原生!然后就出現(xiàn)了需要麥克風錄音機跟H5交互的功能模塊!
查了資料都文章說iOS對h5交互麥克風錄音不友好
現(xiàn)在具體工作流程步驟如下:
- 首先創(chuàng)建了一個wkwebview
//加載webview視圖
override func loadView() {
let preference = WKPreferences()
preference.minimumFontSize = 0
preference.javaScriptEnabled = true
preference.javaScriptCanOpenWindowsAutomatically = true
preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
debugPrint("這里已經(jīng)進來了")
// swift 提供給 h5 調(diào)用方法
let userContentController = WKUserContentController()
userContentController.add(self, name: "callAudio") //調(diào)起iOS音頻權(quán)限
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") //調(diào)起iOS音頻權(quán)限
// 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定義好協(xié)議接收的方法
- 重點:加載完成后接收H5調(diào)用的協(xié)議方法:
// 接受 h5 調(diào)用
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: "請到設(shè)置 -> 隱私 -> 麥克風 ,打開訪問權(quán)限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
alertView.show()
}
}
}
}
}
SystemAuth.authMicrophone 調(diào)用錄音麥克風權(quán)限返回true跟false
self.webView.evaluateJavaScript("getPermission('(result)')", completionHandler: nil) iOS攔截到方法注入新方法getPermission()攜帶參數(shù)true或者false返回給H5接收
Swift開啟iOS的錄音權(quán)限包括其他照相機權(quán)限的代碼文件
我整理好在下面的代碼了
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權(quán)限
import Intents
/// 語音轉(zhuǎn)文字權(quán)限
import Speech
/// 日歷、提醒事項
import EventKit
/// Face、TouchID
import LocalAuthentication
import HealthKit
import HomeKit
/// 運動與健身權(quán)限
import CoreMotion
/// 防止獲取無效 計步器
private let cmPedometer = CMPedometer()
typealias AuthClouser = ((Bool)->())
/// 定義私有全局變量,解決在iOS 13 定位權(quán)限彈框自動消失的問題
private let locationAuthManager = CLLocationManager()
/**
escaping 逃逸閉包的生命周期:
1,閉包作為參數(shù)傳遞給函數(shù);
2,退出函數(shù);
3,閉包被調(diào)用,閉包生命周期結(jié)束
即逃逸閉包的生命周期長于函數(shù),函數(shù)退出的時候,逃逸閉包的引用仍被其他對象持有,不會在函數(shù)結(jié)束時釋放
經(jīng)常使用逃逸閉包的2個場景:
異步調(diào)用: 如果需要調(diào)度隊列中異步調(diào)用閉包,比如網(wǎng)絡(luò)請求成功的回調(diào)和失敗的回調(diào),這個隊列會持有閉包的引用,至于什么時候調(diào)用閉包,或閉包什么時候運行結(jié)束都是不確定,上邊的例子。
存儲: 需要存儲閉包作為屬性,全局變量或其他類型做稍后使用,例子待補充
*/
public class SystemAuth {
// /**
// 媒體資料庫/Apple Music權(quán)限
//
// - parameters: action 權(quán)限結(jié)果閉包
// */
// 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)
// }
// }
// }
// /// 用戶明確拒絕此應(yīng)用程序的授權(quán),或在設(shè)置中禁用該服務(wù)。
// case .denied:
// clouser(false)
// /// 該應(yīng)用程序未被授權(quán)使用該服務(wù)。由于用戶無法改變對該服務(wù)的主動限制。此狀態(tài),并且個人可能沒有拒絕授權(quán)。
// case .restricted:
// clouser(false)
// /// 已授權(quán)
// case .authorized:
// clouser(true)
// /// 擴展以后可能有的狀態(tài),做保護措施
// @unknown default:
// clouser(false)
// }
// }
// /**
// 聯(lián)網(wǎng)權(quán)限
//
// - parameters: action 權(quán)限結(jié)果閉包
// */
// 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)
// }
// }
/**
相機權(quán)限
- parameters: action 權(quán)限結(jié)果閉包
*/
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)
}
}
/**
相冊權(quán)限
- parameters: action 權(quán)限結(jié)果閉包
*/
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)
}
}
/**
麥克風權(quán)限
- parameters: action 權(quán)限結(jié)果閉包
*/
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)
}
}
//開啟麥克風權(quán)限
func openAudioSession() {
let permissionStatus = AVAudioSession.sharedInstance().recordPermission
if permissionStatus == AVAudioSession.RecordPermission.undetermined {
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
//此處可以判斷權(quán)限狀態(tài)來做出相應(yīng)的操作,如改變按鈕狀態(tài)
if granted{
DispatchQueue.main.async {
}
}else{
DispatchQueue.main.async {
let alertView = UIAlertView(title: "無法訪問您的麥克風" , message: "請到設(shè)置 -> 隱私 -> 麥克風 ,打開訪問權(quán)限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
alertView.show()
}
}
}
}
}
//是否開啟麥克風
func getPermission() -> Bool{
let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
return authStatus != .restricted && authStatus != .denied
}
/**
定位權(quán)限
- parameters: action 權(quán)限結(jié)果閉包(有無權(quán)限,是否第一次請求權(quán)限)
*/
class func authLocation(clouser: @escaping ((Bool,Bool)->())) {
let authStatus = CLLocationManager.authorizationStatus()
switch authStatus {
case .notDetermined:
//由于IOS8中定位的授權(quán)機制改變 需要進行手動授權(quán)
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)
}
}
// /**
// 推送權(quán)限
//
// - parameters: action 權(quán)限結(jié)果閉包
// */
// 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 權(quán)限結(jié)果閉包
*/
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)
}
}
}
}
/**
通訊錄權(quán)限
- parameters: action 權(quán)限結(jié)果閉包
*/
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 權(quán)限
//
// - parameters: action 權(quán)限結(jié)果閉包
// */
// 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)
// }
// }
/**
語音轉(zhuǎn)文字權(quán)限
- parameters: action 權(quán)限結(jié)果閉包
*/
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 權(quán)限結(jié)果閉包
*/
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 權(quán)限結(jié)果閉包
*/
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 權(quán)限結(jié)果閉包
*/
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("不可以使用")
}
}
/**
健康 (寫:體能訓(xùn)練、iOS13 聽力圖 讀: 健身記錄、體能訓(xùn)練、iOS13 聽力圖)
- parameters: action 權(quán)限結(jié)果閉包
*/
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)
}
}
/**
家庭、住宅數(shù)據(jù)
- parameters: action 權(quán)限結(jié)果閉包
*/
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)
}
}
}
/**
系統(tǒng)設(shè)置
- parameters: urlString 可以為系統(tǒng),也可以為微信:weixin://、QQ:mqq://
- parameters: action 結(jié)果閉包
*/
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編碼發(fā)送回去給前端,剩下他就能接收處理
let fileData = try! Data(contentsOf: wavFileURL)
//將圖片轉(zhuǎn)為base64編碼
let base64 = fileData.base64EncodedString(options: .endLineWithLineFeed).addingPercentEncoding(withAllowedCharacters: .alphanumerics)
UserDefaults.standard.setValue(base64, forKey: "wavFileURL")
4.到這里,你就以為很成功了,能順利錄音-播放-展示到webview了嗎?那你就繼續(xù)入坑吧
原本以為我這里通過base64把錄完音的文件發(fā)送給前端就沒我活了,誰知道錄完音老是播放不了
解決方法:
- 由于WKWebView無權(quán)限訪問本地文件,訪問本地文件使用的是file://協(xié)議,由于WKWebView的安全機制,會報一些錯無法訪問到。需要打開webView的file://協(xié)議訪問權(quán)限,設(shè)置allowFileAccessFromFileURLs為true。
2.如果出現(xiàn)跨域的報錯,也可以通過設(shè)置allowUniversalAccessFromFileURLs為true來解決。
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 解決HTML請求跨域
[config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];
WKPreferences *preferences = [[WKPreferences alloc] init];
// 打開web訪問本地文件權(quán)限
[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("這里已經(jīng)進來了")

image.png
更多方法交流可以家魏鑫:lixiaowu1129,一起探討iOS相關(guān)技術(shù)!