logo
Développement
Rechercher
Guide d'intégration Workspace pour iOS

Guide d'intégration Workspace pour iOS

Ce guide fournit des instructions détaillées sur la façon d'intégrer l'espace de travail GPTBots dans des applications iOS, y compris les demandes d'autorisations, les interactions natif-vers-H5 et d'autres configurations pertinentes.
GPTBots propose également un projet de démonstration d'espace de travail pour vous aider à démarrer rapidement. Projet de démo iOS : Projet de démo iOS

Exigences de configuration de base

  • iOS 13.0 ou supérieur
  • Xcode 12.0 ou supérieur
  • Swift 5.0 ou supérieur

Ajout des frameworks requis

Le projet nécessite les frameworks suivants :

  • WebKit
  • AVFoundation (pour l'accès au microphone)
  • Photos (pour l'accès à la photothèque)
  • MobileCoreServices (pour la sélection de fichiers)

Pour iOS 14 et plus, vous pouvez aussi utiliser :

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

                    
Ce bloc de code dans la fenêtre flottante

Configuration du fichier Info.plist

Ajoutez les configurations suivantes à Info.plist :

<!-- Autoriser les requêtes 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>
                      
                      <!-- Autoriser les requêtes 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>

                    
Ce bloc de code dans la fenêtre flottante

Configuration des autorisations

L'application nécessite les autorisations suivantes :

Autorisation appareil photo

<key>NSCameraUsageDescription</key> <string>Cette application doit utiliser l'appareil photo pour permettre la capture de photos dans les pages web</string>
                      
                      <key>NSCameraUsageDescription</key>
<string>Cette application doit utiliser l'appareil photo pour permettre la capture de photos dans les pages web</string>

                    
Ce bloc de code dans la fenêtre flottante

Autorisation microphone

<key>NSMicrophoneUsageDescription</key> <string>Cette application doit utiliser le microphone pour permettre l'enregistrement audio dans les pages web</string>
                      
                      <key>NSMicrophoneUsageDescription</key>
<string>Cette application doit utiliser le microphone pour permettre l'enregistrement audio dans les pages web</string>

                    
Ce bloc de code dans la fenêtre flottante

Autorisation photothèque

<key>NSPhotoLibraryUsageDescription</key> <string>Cette application doit accéder à votre photothèque pour sélectionner des images et des fichiers vidéo</string>
                      
                      <key>NSPhotoLibraryUsageDescription</key>
<string>Cette application doit accéder à votre photothèque pour sélectionner des images et des fichiers vidéo</string>

                    
Ce bloc de code dans la fenêtre flottante

Autorisation d'accès aux documents

<key>NSDocumentUsageDescription</key> <string>Cette application doit accéder aux documents pour permettre l'envoi de fichiers dans les pages web</string>
                      
                      <key>NSDocumentUsageDescription</key>
<string>Cette application doit accéder aux documents pour permettre l'envoi de fichiers dans les pages web</string>

                    
Ce bloc de code dans la fenêtre flottante

Interaction entre natif et WebView

Création d'un contrôleur WebView

Créez un contrôleur dédié pour afficher 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)
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Implémentation du pont JavaScript

Créez une classe de pont pour gérer la communication entre le code natif et 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)
        }
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Implémentation des méthodes déléguées WebView

Implémentez les méthodes déléguées nécessaires pour gérer la sélection de fichiers, les demandes d'autorisations, etc. :

extension WebViewController: WKUIDelegate { // Gérer l'envoi de fichiers @available(iOS 18.4, *) func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { let alert = UIAlertController(title: "Sélectionner un fichier", message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Sélectionner depuis Photos", style: .default) { _ in self.presentImagePicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "Sélectionner depuis Fichiers", style: .default) { _ in self.presentDocumentPicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "Annuler", style: .cancel) { _ in completionHandler(nil) }) present(alert, animated: true) } // Gérer les demandes d'autorisations médias @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { switch type { case .microphone: // Vérifier l'autorisation du 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 {
    // Gérer l'envoi de fichiers
    @available(iOS 18.4, *)
    func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, 
                 initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) {
        
        let alert = UIAlertController(title: "Sélectionner un fichier", message: nil, preferredStyle: .actionSheet)
        
        alert.addAction(UIAlertAction(title: "Sélectionner depuis Photos", style: .default) { _ in
            self.presentImagePicker(completionHandler: completionHandler)
        })
        
        alert.addAction(UIAlertAction(title: "Sélectionner depuis Fichiers", style: .default) { _ in
            self.presentDocumentPicker(completionHandler: completionHandler)
        })
        
        alert.addAction(UIAlertAction(title: "Annuler", style: .cancel) { _ in
            completionHandler(nil)
        })
        
        present(alert, animated: true)
    }
    
    // Gérer les demandes d'autorisations médias
    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, 
                 initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, 
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        
        switch type {
        case .microphone:
            // Vérifier l'autorisation du 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)
        }
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Implémentation de la fonctionnalité de sélection de fichiers

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)
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Gestion des messages 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] ?? [:] // Dispatcher le traitement de l'événement sur le thread principal 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("Erreur lors de l'analyse du message H5 : \(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] ?? [:]
            
            // Dispatcher le traitement de l'événement sur le thread principal
            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("Erreur lors de l'analyse du message H5 : \(error.localizedDescription)")
        }
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Dépannage

1. La WebView ne charge pas la page

  • Vérifiez la connexion réseau.
  • Vérifiez que l'entrée NSAppTransportSecurity dans Info.plist est correctement configurée.
  • Assurez-vous que l'URL est bien formée et accessible.

2. Les demandes d'autorisations sont refusées

  • Vérifiez que chaque clé de description d'utilisation requise est présente dans Info.plist.
  • Indiquez aux utilisateurs comment activer manuellement l'autorisation dans Réglages > Confidentialité si la demande initiale a été refusée.

3. La communication du pont JavaScript est rompue

  • Vérifiez que WebViewBridge a bien enregistré son interface JavaScript avec le WKUserContentController.
  • Validez que la charge utile du message JavaScript correspond au schéma JSON attendu.
  • Utilisez l'inspecteur Web de Safari (menu Développement) pour déboguer les erreurs JavaScript dans la WKWebView.

4. Problèmes d'envoi de fichiers

  • Sur iOS 13 et versions antérieures, utilisez les bons UTI (par exemple, public.image, public.data) lors de la présentation du sélecteur de documents.
  • Vérifiez que l'application dispose des autorisations de confidentialité nécessaires pour l'accès aux Photos et aux Fichiers.
  • Conservez l'URL du fichier sélectionné jusqu'à la fin de l'envoi ; évitez d'accéder à des URLs à portée de sécurité expirée.