logo
Desarrollo
Buscar
Documento de instrucciones para la integración del espacio de trabajo en iOS

Documento de instrucciones para la integración del espacio de trabajo en iOS

Esta guía presenta instrucciones detalladas sobre cómo integrar el espacio de trabajo de GPTBots en aplicaciones iOS, incluida la solicitud de permisos, la interacción entre el código nativo y H5, así como otras configuraciones relacionadas.

GPTBots también proporciona un proyecto DEMO del espacio de trabajo que puede ayudarle a comenzar rápidamente. Proyecto DEMO para iOS: ios-webview-bridge


Requisitos básicos de configuración

  • iOS 13.0 o superior
  • Xcode 12.0 o superior
  • Swift 5.0 o superior

Añadir los frameworks necesarios

El proyecto requiere los siguientes frameworks:

  • WebKit
  • AVFoundation (para el acceso al micrófono)
  • Photos (para el acceso al álbum de fotos)
  • MobileCoreServices (para la selección de archivos)

En iOS 14 y versiones superiores, también se puede utilizar:

#if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif
                      
                      #if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif

                    
Este bloque de código en una ventana flotante

Configurar Info.plist

Añada la siguiente configuración en Info.plist:

<!-- 允许HTTP请求 --> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> <key>NSExceptionDomains</key> <dict> <key>gptbots-auto.qa.jpushoa.com</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSExceptionMinimumTLSVersion</key> <string>TLSv1.0</string> <key>NSIncludesSubdomains</key> <true/> </dict> </dict> </dict>
                      
                      <!-- 允许HTTP请求 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>gptbots-auto.qa.jpushoa.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.0</string>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

                    
Este bloque de código en una ventana flotante

Configuración de permisos

La aplicación requiere los siguientes permisos:

Permiso de cámara

<key>NSCameraUsageDescription</key> <string>This app needs to use the camera to support photo capture features in web pages</string>
                      
                      <key>NSCameraUsageDescription</key>
<string>This app needs to use the camera to support photo capture features in web pages</string>

                    
Este bloque de código en una ventana flotante

Permiso de micrófono

<key>NSMicrophoneUsageDescription</key> <string>This app needs to use the microphone to support audio recording features in web pages</string>
                      
                      <key>NSMicrophoneUsageDescription</key>
<string>This app needs to use the microphone to support audio recording features in web pages</string>

                    
Este bloque de código en una ventana flotante

Permiso de álbum de fotos

<key>NSPhotoLibraryUsageDescription</key> <string>This app needs to access your photo library to select images and video files</string>
                      
                      <key>NSPhotoLibraryUsageDescription</key>
<string>This app needs to access your photo library to select images and video files</string>

                    
Este bloque de código en una ventana flotante

Permiso de acceso a documentos

<key>NSDocumentUsageDescription</key> <string>This app needs to access documents to support file upload features in web pages</string>
                      
                      <key>NSDocumentUsageDescription</key>
<string>This app needs to access documents to support file upload features in web pages</string>

                    
Este bloque de código en una ventana flotante

Interacción entre el código nativo y el WebView

Crear el controlador del WebView

Cree un controlador dedicado a mostrar el WebView:

class WebViewController: UIViewController { private var webView: WKWebView! private var webViewBridge: WebViewBridge! var urlString: String = "" override func viewDidLoad() { super.viewDidLoad() setupWebView() setupWebViewBridge() loadURL() } private func setupWebView() { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] configuration.applicationNameForUserAgent = "WebViewApp/1.0" webView = WKWebView(frame: view.bounds, configuration: configuration) webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] webView.navigationDelegate = self webView.uiDelegate = self view.addSubview(webView) } private func loadURL() { guard !urlString.isEmpty, let url = URL(string: urlString) else { return } let request = URLRequest(url: url) webView.load(request) } }
                      
                      class WebViewController: UIViewController {
    private var webView: WKWebView!
    private var webViewBridge: WebViewBridge!
    var urlString: String = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
        setupWebViewBridge()
        loadURL()
    }

    private func setupWebView() {
        let configuration = WKWebViewConfiguration()
        configuration.allowsInlineMediaPlayback = true
        configuration.mediaTypesRequiringUserActionForPlayback = []
        configuration.applicationNameForUserAgent = "WebViewApp/1.0"

        webView = WKWebView(frame: view.bounds, configuration: configuration)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.navigationDelegate = self
        webView.uiDelegate = self

        view.addSubview(webView)
    }

    private func loadURL() {
        guard !urlString.isEmpty, let url = URL(string: urlString) else {
            return
        }

        let request = URLRequest(url: url)
        webView.load(request)
    }
}

                    
Este bloque de código en una ventana flotante

Implementar el puente de JavaScript

Cree una clase de puente para la comunicación entre el código nativo y JavaScript:

class WebViewBridge: NSObject { static let EVENT_CLICK = "click" static let EVENT_MESSAGE = "message" private weak var webView: WKWebView? weak var delegate: WebViewBridgeDelegate? init(webView: WKWebView, viewController: UIViewController) { self.webView = webView super.init() } func registerJSInterface() { webView?.configuration.userContentController.add(self, name: "agentWebBridge") } func callH5(eventType: String, data: [String: Any]) { let message: [String: Any] = [ "eventType": eventType, "data": data ] if let jsonData = try? JSONSerialization.data(withJSONObject: message), let jsonString = String(data: jsonData, encoding: .utf8) { let escapedJson = jsonString .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "'", with: "\\'") .replacingOccurrences(of: "\"", with: "\\\"") let jsCode = "window.onCallH5Message('\(escapedJson)')" webView?.evaluateJavaScript(jsCode) } } }
                      
                      class WebViewBridge: NSObject {
    static let EVENT_CLICK = "click"
    static let EVENT_MESSAGE = "message"

    private weak var webView: WKWebView?
    weak var delegate: WebViewBridgeDelegate?

    init(webView: WKWebView, viewController: UIViewController) {
        self.webView = webView
        super.init()
    }

    func registerJSInterface() {
        webView?.configuration.userContentController.add(self, name: "agentWebBridge")
    }

    func callH5(eventType: String, data: [String: Any]) {
        let message: [String: Any] = [
            "eventType": eventType,
            "data": data
        ]

        if let jsonData = try? JSONSerialization.data(withJSONObject: message),
           let jsonString = String(data: jsonData, encoding: .utf8) {
            let escapedJson = jsonString
                .replacingOccurrences(of: "\\", with: "\\\\")
                .replacingOccurrences(of: "'", with: "\\'")
                .replacingOccurrences(of: "\"", with: "\\\"")

            let jsCode = "window.onCallH5Message('\(escapedJson)')"

            webView?.evaluateJavaScript(jsCode)
        }
    }
}

                    
Este bloque de código en una ventana flotante

Implementar los métodos delegados del WebView

Implemente los métodos delegados necesarios del WebView para gestionar la selección de archivos, las solicitudes de permisos, etc.:

extension WebViewController: WKUIDelegate { // 处理文件上传 @available(iOS 18.4, *) func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { let alert = UIAlertController(title: "Select File", message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Select from Photos", style: .default) { _ in self.presentImagePicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "Select from Files", style: .default) { _ in self.presentDocumentPicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completionHandler(nil) }) present(alert, animated: true) } // 处理媒体权限请求 @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { switch type { case .microphone: // 检查麦克风权限 switch AVAudioSession.sharedInstance().recordPermission { case .granted: decisionHandler(.grant) case .denied: decisionHandler(.deny) case .undetermined: AVAudioSession.sharedInstance().requestRecordPermission { granted in DispatchQueue.main.async { if granted { decisionHandler(.grant) } else { decisionHandler(.deny) } } } @unknown default: decisionHandler(.deny) } case .camera, .cameraAndMicrophone: decisionHandler(.grant) @unknown default: decisionHandler(.deny) } } }
                      
                      extension WebViewController: WKUIDelegate {
    // 处理文件上传
    @available(iOS 18.4, *)
    func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters,
                 initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) {

        let alert = UIAlertController(title: "Select File", message: nil, preferredStyle: .actionSheet)

        alert.addAction(UIAlertAction(title: "Select from Photos", style: .default) { _ in
            self.presentImagePicker(completionHandler: completionHandler)
        })

        alert.addAction(UIAlertAction(title: "Select from Files", style: .default) { _ in
            self.presentDocumentPicker(completionHandler: completionHandler)
        })

        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
            completionHandler(nil)
        })

        present(alert, animated: true)
    }

    // 处理媒体权限请求
    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin,
                 initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType,
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {

        switch type {
        case .microphone:
            // 检查麦克风权限
            switch AVAudioSession.sharedInstance().recordPermission {
            case .granted:
                decisionHandler(.grant)
            case .denied:
                decisionHandler(.deny)
            case .undetermined:
                AVAudioSession.sharedInstance().requestRecordPermission { granted in
                    DispatchQueue.main.async {
                        if granted {
                            decisionHandler(.grant)
                        } else {
                            decisionHandler(.deny)
                        }
                    }
                }
            @unknown default:
                decisionHandler(.deny)
            }
        case .camera, .cameraAndMicrophone:
            decisionHandler(.grant)
        @unknown default:
            decisionHandler(.deny)
        }
    }
}

                    
Este bloque de código en una ventana flotante

Implementar la funcionalidad de selección de archivos

extension WebViewController { private func presentImagePicker(completionHandler: @escaping ([URL]?) -> Void) { let picker = UIImagePickerController() picker.delegate = self picker.sourceType = .photoLibrary picker.mediaTypes = ["public.image", "public.movie"] fileUploadCallback = { url in if let url = url { completionHandler([url]) } else { completionHandler(nil) } } present(picker, animated: true) } private func presentDocumentPicker(completionHandler: @escaping ([URL]?) -> Void) { let picker: UIDocumentPickerViewController if #available(iOS 14.0, *) { picker = UIDocumentPickerViewController(forOpeningContentTypes: [.data, .text, .image, .movie, .audio]) } else { picker = UIDocumentPickerViewController(documentTypes: [ "public.data", "public.text", "public.image", "public.movie", "public.audio" ], in: .import) } picker.delegate = self picker.allowsMultipleSelection = false fileUploadCallback = { url in if let url = url { completionHandler([url]) } else { completionHandler(nil) } } present(picker, animated: true) } }
                      
                      extension WebViewController {
    private func presentImagePicker(completionHandler: @escaping ([URL]?) -> Void) {
        let picker = UIImagePickerController()
        picker.delegate = self
        picker.sourceType = .photoLibrary
        picker.mediaTypes = ["public.image", "public.movie"]

        fileUploadCallback = { url in
            if let url = url {
                completionHandler([url])
            } else {
                completionHandler(nil)
            }
        }

        present(picker, animated: true)
    }

    private func presentDocumentPicker(completionHandler: @escaping ([URL]?) -> Void) {
        let picker: UIDocumentPickerViewController

        if #available(iOS 14.0, *) {
            picker = UIDocumentPickerViewController(forOpeningContentTypes: [.data, .text, .image, .movie, .audio])
        } else {
            picker = UIDocumentPickerViewController(documentTypes: [
                "public.data",
                "public.text",
                "public.image",
                "public.movie",
                "public.audio"
            ], in: .import)
        }

        picker.delegate = self
        picker.allowsMultipleSelection = false

        fileUploadCallback = { url in
            if let url = url {
                completionHandler([url])
            } else {
                completionHandler(nil)
            }
        }

        present(picker, animated: true)
    }
}

                    
Este bloque de código en una ventana flotante

Gestionar los mensajes del WebView

extension WebViewBridge: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "agentWebBridge", let messageBody = message.body as? String else { return } do { guard let data = messageBody.data(using: .utf8), let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let eventType = jsonObject["eventType"] as? String else { return } let eventData = jsonObject["data"] as? [String: Any] ?? [:] // 根据事件类型分发处理 DispatchQueue.main.async { [weak self] in guard let self = self else { return } switch eventType { case WebViewBridge.EVENT_CLICK: self.delegate?.onClickEvent(data: eventData) case WebViewBridge.EVENT_MESSAGE: self.delegate?.onMessageEvent(data: eventData) default: self.delegate?.onUnhandledEvent(eventType: eventType, data: eventData) } } } catch { print("Error parsing H5 message: \(error.localizedDescription)") } } }
                      
                      extension WebViewBridge: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard message.name == "agentWebBridge",
              let messageBody = message.body as? String else {
            return
        }

        do {
            guard let data = messageBody.data(using: .utf8),
                  let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
                  let eventType = jsonObject["eventType"] as? String else {
                return
            }

            let eventData = jsonObject["data"] as? [String: Any] ?? [:]

            // 根据事件类型分发处理
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }

                switch eventType {
                case WebViewBridge.EVENT_CLICK:
                    self.delegate?.onClickEvent(data: eventData)

                case WebViewBridge.EVENT_MESSAGE:
                    self.delegate?.onMessageEvent(data: eventData)

                default:
                    self.delegate?.onUnhandledEvent(eventType: eventType, data: eventData)
                }
            }
        } catch {
            print("Error parsing H5 message: \(error.localizedDescription)")
        }
    }
}

                    
Este bloque de código en una ventana flotante

Preguntas frecuentes

1. El WebView no puede cargar la página

  • Compruebe la conexión de red
  • Confirme que la configuración de NSAppTransportSecurity en Info.plist es correcta
  • Verifique que el formato de la URL es correcto

2. La solicitud de permisos es denegada

  • Asegúrese de haber añadido en Info.plist todas las descripciones de permisos necesarias
  • Guíe al usuario para que active los permisos manualmente en la configuración del dispositivo

3. Fallo en la interacción con JavaScript

  • Asegúrese de que WebViewBridge haya registrado correctamente la interfaz de JavaScript
  • Compruebe que el formato del mensaje de JavaScript cumple con lo esperado
  • Utilice las herramientas de desarrollador de Safari para depurar los errores de JavaScript en el WebView

4. Problemas de carga de archivos

  • Para versiones inferiores a iOS 14, asegúrese de utilizar los identificadores de tipo de documento correctos
  • Asegúrese de que la aplicación tenga permisos suficientes para acceder al álbum de fotos y al sistema de archivos
  • Gestione el ciclo de vida de la URL del archivo, asegurándose de que la URL siga siendo válida en el momento de su uso

Documentos relacionados