iOS Swift H5 WKWebView交互麥克風錄音完訪問本地文件路徑遇到的問題及解決方案

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

需求分析:

最近項目需求需要麥克風錄音權(quán)限,因為整體上的UI界面是前端wkwebview搭建的,實現(xiàn)功能邏輯是由iOS實現(xiàn),沒有用原生!然后就出現(xiàn)了需要麥克風錄音機跟H5交互的功能模塊!

查了資料都文章說iOS對h5交互麥克風錄音不友好
現(xiàn)在具體工作流程步驟如下:

  1. 首先創(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é)議接收的方法

  1. 重點:加載完成后接收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)
        }
    }
}

  1. 重點來了 錄制完音頻文件后,也是跟前端定義好方法返回回去
    讓前端利用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ā)送給前端就沒我活了,誰知道錄完音老是播放不了
解決方法:

  1. 由于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ù)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容