logo
Développement
Rechercher
Documentation d'intégration de l'espace de travail sur iOS

Documentation d'intégration de l'espace de travail sur iOS

Ce guide présente en détail comment intégrer l'espace de travail GPTBots dans une application iOS, y compris la demande d'autorisations, l'interaction entre le natif et le H5 ainsi que d'autres configurations connexes.

GPTBots fournit également un projet de DÉMO de l'espace de travail, qui peut vous aider à démarrer rapidement. Projet de DÉMO pour iOS : ios-webview-bridge


Exigences de configuration de base

  • iOS 13.0 ou version ultérieure
  • Xcode 12.0 ou version ultérieure
  • Swift 5.0 ou version ultérieure

Ajouter les frameworks nécessaires

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)

Sur iOS 14 et versions ultérieures, vous pouvez également utiliser :

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

                    
Ce bloc de code dans la fenêtre flottante

Configurer Info.plist

Ajoutez la configuration suivante dans 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>

                    
Ce bloc de code dans la fenêtre flottante

Configuration des autorisations

L'application nécessite les autorisations suivantes :

Autorisation caméra

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

                    
Ce bloc de code dans la fenêtre flottante

Autorisation microphone

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

                    
Ce bloc de code dans la fenêtre flottante

Autorisation photothèque

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

                    
Ce bloc de code dans la fenêtre flottante

Autorisation d'accès aux documents

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

                    
Ce bloc de code dans la fenêtre flottante

Interaction entre le natif et la WebView

Créer le contrôleur de WebView

Créez un contrôleur dédié à l'affichage de 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émenter le pont JavaScript

Créez une classe de pont, utilisée pour la communication entre le code natif et le 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émenter les méthodes déléguées de la WebView

Implémentez les méthodes déléguées nécessaires de la WebView pour gérer la sélection de fichiers, les demandes d'autorisation, 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)
        }
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Implémenter la fonction 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

Traiter les messages de la 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)")
        }
    }
}

                    
Ce bloc de code dans la fenêtre flottante

Questions fréquentes

1. La WebView ne parvient pas à charger la page

  • Vérifiez la connexion réseau
  • Confirmez que la configuration NSAppTransportSecurity dans Info.plist est correcte
  • Vérifiez que le format de l'URL est correct

2. La demande d'autorisation est refusée

  • Assurez-vous d'avoir ajouté toutes les descriptions d'autorisations nécessaires dans Info.plist
  • Guidez l'utilisateur pour activer manuellement les autorisations dans les réglages de l'appareil

3. L'interaction JavaScript échoue

  • Assurez-vous que WebViewBridge a correctement enregistré l'interface JavaScript
  • Vérifiez que le format des messages JavaScript est conforme à ce qui est attendu
  • Utilisez les outils de développement Safari pour déboguer les erreurs JavaScript dans la WebView

4. Problèmes de téléversement de fichiers

  • Pour les versions antérieures à iOS 14, assurez-vous d'utiliser les identifiants de type de document corrects
  • Assurez-vous que l'application dispose des autorisations suffisantes pour accéder à la photothèque et au système de fichiers
  • Gérez le cycle de vie de l'URL du fichier, en veillant à ce que l'URL soit toujours valide au moment de son utilisation

Documents connexes