logo
开发者文档
搜索
iOS 端使用工作空间集成说明文档

iOS 端使用工作空间集成说明文档

本指南介绍了如何在 iOS 应用中集成 GPTBots 工作空间的详细说明,包括权限申请、原生与 H5 交互以及其他相关配置。

GPTBots 同时提供了工作空间的 DEMO 项目,可以帮助您快速开始。iOS 端 DEMO 项目:ios-webview-bridge


基本配置要求

  • iOS 13.0 或更高版本
  • Xcode 12.0 或更高版本
  • Swift 5.0 或更高版本

添加必要的框架

项目需要以下框架:

  • WebKit
  • AVFoundation(用于麦克风访问)
  • Photos(用于相册访问)
  • MobileCoreServices(用于文件选择)

在 iOS 14 及以上版本,还可以使用:

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

                    
此代码块在浮窗中显示

配置 Info.plist

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

                    
此代码块在浮窗中显示

权限配置

应用需要以下权限:

相机权限

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

                    
此代码块在浮窗中显示

麦克风权限

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

                    
此代码块在浮窗中显示

相册权限

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

                    
此代码块在浮窗中显示

文档访问权限

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

                    
此代码块在浮窗中显示

原生与 WebView 交互

创建 WebView 控制器

创建一个专门用于显示 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)
    }
}

                    
此代码块在浮窗中显示

实现 JavaScript 桥接

创建一个桥接类,用于原生代码与 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)
        }
    }
}

                    
此代码块在浮窗中显示

实现 WebView 代理方法

实现必要的 WebView 代理方法,处理文件选择、权限请求等:

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

                    
此代码块在浮窗中显示

实现文件选择功能

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

                    
此代码块在浮窗中显示

处理 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)")
        }
    }
}

                    
此代码块在浮窗中显示

常见问题

1. WebView 无法加载页面

  • 检查网络连接
  • 确认 Info.plist 中的 NSAppTransportSecurity 配置正确
  • 验证 URL 格式是否正确

2. 权限请求被拒绝

  • 确保在 Info.plist 中添加了所有必要的权限描述
  • 引导用户在设备设置中手动开启权限

3. JavaScript 交互失败

  • 确保 WebViewBridge 正确注册了 JavaScript 接口
  • 检查 JavaScript 消息格式是否符合预期
  • 使用 Safari 开发者工具调试 WebView 中的 JavaScript 错误

4. 文件上传问题

  • 对于 iOS 14 以下版本,确保使用正确的文档类型标识符
  • 确保应用有足够的权限访问相册和文件系统
  • 处理文件 URL 的生命周期,确保 URL 在使用时仍然有效

相关文档