logo
Desarrollo
Buscar
Workspace Integration Guide for iOS

Workspace Integration Guide for iOS

This guide provides detailed instructions on how to integrate GPTBots workspace into iOS applications, including permission requests, native-to-H5 interactions, and other relevant configurations.
GPTBots also provides a workspace DEMO project to help you get started quickly. iOS DEMO project: iOS DEMO Project

Basic Configuration Requirements

  • iOS 13.0 or higher
  • Xcode 12.0 or higher
  • Swift 5.0 or higher

Adding Required Frameworks

The project requires the following frameworks:

  • WebKit
  • AVFoundation (for microphone access)
  • Photos (for photo library access)
  • MobileCoreServices (for file selection)

For iOS 14 and above, you can also use:

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

                    
Este bloque de código en una ventana flotante

Configuring Info.plist

Add the following configurations to 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

Permission Configuration

The application requires the following permissions:

Camera Permission

<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

Microphone Permission

<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

Photo Library Permission

<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

Document Access Permission

<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

Native and WebView Interaction

Creating a WebView Controller

Create a dedicated controller to display the 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

Implementing JavaScript Bridge

Create a bridge class to handle communication between native code and 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

Implementing WebView Delegate Methods

Implement necessary WebView delegate methods to handle file selection, permission requests, 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

Implementing File Selection Functionality

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

Handling WebView Messages

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

Troubleshooting

1. WebView Fails to Load the Page

  • Check the network connection.
  • Verify that the NSAppTransportSecurity entry in Info.plist is correctly configured.
  • Make sure the URL is well-formed and reachable.

2. Permission Requests Are Denied

  • Ensure every required usage-description key is present in Info.plist.
  • Guide users to manually enable the permission in Settings > Privacy if the initial request was denied.

3. JavaScript Bridge Communication Breaks

  • Confirm that WebViewBridge has registered its JavaScript interface with the WKUserContentController.
  • Validate that the message payload from JavaScript matches the expected JSON schema.
  • Use Safari’s Web Inspector (Develop menu) to debug JavaScript errors inside the WKWebView.

4. File-Upload Issues

  • On iOS 13 and earlier, use the correct UTIs (e.g., public.image, public.data) when presenting the document picker.
  • Ensure the app holds the necessary privacy entitlements for Photos and Files access.
  • Retain the selected file URL until the upload completes; avoid accessing stale security-scoped URLs.