logo
Desarrollo
Buscar
Guía de integración del espacio de trabajo para iOS

Guía de integración del espacio de trabajo para iOS

Esta guía proporciona instrucciones detalladas sobre cómo integrar el espacio de trabajo de GPTBots en aplicaciones iOS, incluidas las solicitudes de permisos, las interacciones entre código nativo y H5 y otras configuraciones relevantes.
GPTBots también proporciona un proyecto de demostración (DEMO) del espacio de trabajo para ayudarle a empezar rápidamente. Proyecto DEMO de iOS: Proyecto DEMO de iOS

Requisitos básicos de configuración

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

Adición de los frameworks necesarios

El proyecto requiere los siguientes frameworks:

  • WebKit
  • AVFoundation (para acceso al micrófono)
  • Photos (para acceso a la fototeca/biblioteca de fotos)
  • MobileCoreServices (para selección de archivos)

Para iOS 14 o superior, 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

Configuración de Info.plist

Añadir la siguiente configuración a Info.plist:

<!-- Allow HTTP requests --> <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>
                      
                      <!-- Allow HTTP requests -->
<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>Esta aplicación necesita utilizar la cámara para admitir las funciones de captura de fotos en páginas web</string>
                      
                      <key>NSCameraUsageDescription</key>
<string>Esta aplicación necesita utilizar la cámara para admitir las funciones de captura de fotos en páginas web</string>

                    
Este bloque de código en una ventana flotante

Permiso de micrófono

<key>NSMicrophoneUsageDescription</key> <string>Esta aplicación necesita utilizar el micrófono para admitir las funciones de grabación de audio en páginas web</string>
                      
                      <key>NSMicrophoneUsageDescription</key>
<string>Esta aplicación necesita utilizar el micrófono para admitir las funciones de grabación de audio en páginas web</string>

                    
Este bloque de código en una ventana flotante

Permiso de fototeca

<key>NSPhotoLibraryUsageDescription</key> <string>Esta aplicación necesita acceder a su fototeca para seleccionar imágenes y archivos de vídeo</string>
                      
                      <key>NSPhotoLibraryUsageDescription</key>
<string>Esta aplicación necesita acceder a su fototeca para seleccionar imágenes y archivos de vídeo</string>

                    
Este bloque de código en una ventana flotante

Permiso de acceso a documentos

<key>NSDocumentUsageDescription</key> <string>Esta aplicación necesita acceder a documentos para admitir las funciones de carga de archivos en páginas web</string>
                      
                      <key>NSDocumentUsageDescription</key>
<string>Esta aplicación necesita acceder a documentos para admitir las funciones de carga de archivos en páginas web</string>

                    
Este bloque de código en una ventana flotante

Interacción entre nativo y WebView

Creación de un controlador de WebView

Crear un controlador dedicado para mostrar la 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

Implementación del puente JavaScript

Crear una clase de puente para gestionar 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

Implementación de métodos delegados de WebView

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

extension WebViewController: WKUIDelegate { // Handle file upload @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) } // Handle media permission requests @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { switch type { case .microphone: // Check microphone permission 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 {
    // Handle file upload
    @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)
    }
    
    // Handle media permission requests
    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, 
                 initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, 
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        
        switch type {
        case .microphone:
            // Check microphone permission
            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

Implementación de 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

Gestión de mensajes de 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] ?? [:] // Dispatch event handling on main thread 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] ?? [:]
            
            // Dispatch event handling on main thread
            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

Solución de problemas

1. WebView no carga la página

  • Comprobar la conexión de red.
  • Verificar que la entrada NSAppTransportSecurity en Info.plist esté configurada correctamente.
  • Asegurarse de que la URL tenga un formato correcto y sea accesible.

2. Se deniegan las solicitudes de permisos

  • Asegurarse de que todas las claves usage-description requeridas estén presentes en Info.plist.
  • Guiar a los usuarios para que habiliten manualmente el permiso en Ajustes > Privacidad si la solicitud inicial se denegó.

3. Se interrumpe la comunicación del puente JavaScript

  • Confirmar que WebViewBridge haya registrado su interfaz de JavaScript en WKUserContentController.
  • Validar que la carga útil del mensaje desde JavaScript coincida con el esquema JSON esperado.
  • Utilizar Web Inspector de Safari (menú Develop (Desarrollo)) para depurar errores de JavaScript dentro de WKWebView.

4. Problemas de carga de archivos

  • En iOS 13 y versiones anteriores, utilizar los UTI correctos (p. ej., public.image, public.data) al presentar el selector de documentos.
  • Asegurarse de que la aplicación disponga de los entitlements de privacidad necesarios para el acceso a Photos y Files.
  • Conservar la URL del archivo seleccionado hasta que la carga se complete; evitar acceder a URL con ámbito de seguridad obsoleto.