คู่มือการเชื่อมต่อ Workspace สำหรับ iOS

คู่มือการเชื่อมต่อ Workspace สำหรับ iOS

คู่มือนี้ให้คำแนะนำโดยละเอียดเกี่ยวกับวิธีเชื่อมต่อ workspace ของ GPTBots เข้ากับแอป iOS ครอบคลุมการขอสิทธิ์ การสื่อสารระหว่าง native กับ H5 และการตั้งค่าอื่น ๆ ที่เกี่ยวข้อง
GPTBots มีโปรเจกต์ workspace DEMO สำหรับเริ่มต้นอย่างรวดเร็ว ตัวอย่าง iOS DEMO: iOS DEMO Project

ข้อกำหนดเบื้องต้น

  • iOS 13.0 ขึ้นไป
  • Xcode 12.0 ขึ้นไป
  • Swift 5.0 ขึ้นไป

การเพิ่ม Framework ที่จำเป็น

โปรเจกต์ต้องใช้ Framework ดังนี้:

  • WebKit
  • AVFoundation (สำหรับไมโครโฟน)
  • Photos (สำหรับคลังรูปภาพ)
  • MobileCoreServices (สำหรับเลือกไฟล์)

สำหรับ iOS 14 ขึ้นไป ใช้:

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

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

การตั้งค่า Info.plist

เพิ่มการตั้งค่าต่อไปนี้ใน Info.plist:

<!-- อนุญาต 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>
                      
                      <!-- อนุญาต 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>

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

การตั้งค่าสิทธิ์

แอปต้องขอสิทธิ์เข้าถึงดังนี้:

กล้อง

<key>NSCameraUsageDescription</key> <string>แอปนี้จำเป็นต้องใช้กล้องเพื่อรองรับฟีเจอร์ถ่ายภาพในหน้าเว็บ</string>
                      
                      <key>NSCameraUsageDescription</key>
<string>แอปนี้จำเป็นต้องใช้กล้องเพื่อรองรับฟีเจอร์ถ่ายภาพในหน้าเว็บ</string>

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

ไมโครโฟน

<key>NSMicrophoneUsageDescription</key> <string>แอปนี้จำเป็นต้องใช้ไมโครโฟนเพื่อรองรับฟีเจอร์บันทึกเสียงในหน้าเว็บ</string>
                      
                      <key>NSMicrophoneUsageDescription</key>
<string>แอปนี้จำเป็นต้องใช้ไมโครโฟนเพื่อรองรับฟีเจอร์บันทึกเสียงในหน้าเว็บ</string>

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

คลังรูปภาพ

<key>NSPhotoLibraryUsageDescription</key> <string>แอปนี้จำเป็นต้องเข้าถึงคลังรูปภาพของคุณเพื่อเลือกไฟล์รูปภาพและวิดีโอ</string>
                      
                      <key>NSPhotoLibraryUsageDescription</key>
<string>แอปนี้จำเป็นต้องเข้าถึงคลังรูปภาพของคุณเพื่อเลือกไฟล์รูปภาพและวิดีโอ</string>

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

เอกสาร

<key>NSDocumentUsageDescription</key> <string>แอปนี้จำเป็นต้องเข้าถึงเอกสารเพื่อรองรับฟีเจอร์อัปโหลดไฟล์ในหน้าเว็บ</string>
                      
                      <key>NSDocumentUsageDescription</key>
<string>แอปนี้จำเป็นต้องเข้าถึงเอกสารเพื่อรองรับฟีเจอร์อัปโหลดไฟล์ในหน้าเว็บ</string>

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

การสื่อสาร Native กับ WebView

สร้าง WebView Controller

สร้างคอนโทรลเลอร์สำหรับแสดง 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 Bridge

สร้างคลาส bridge สำหรับรับ-ส่งข้อมูลระหว่าง native กับ 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 Delegate Methods

เพิ่มเมธอด delegate สำหรับเลือกไฟล์ ขอสิทธิ์ ฯลฯ:

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: "เลือกไฟล์", message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "เลือกจากรูปภาพ", style: .default) { _ in self.presentImagePicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "เลือกจากไฟล์", style: .default) { _ in self.presentDocumentPicker(completionHandler: completionHandler) }) alert.addAction(UIAlertAction(title: "ยกเลิก", 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: "เลือกไฟล์", message: nil, preferredStyle: .actionSheet)
        
        alert.addAction(UIAlertAction(title: "เลือกจากรูปภาพ", style: .default) { _ in
            self.presentImagePicker(completionHandler: completionHandler)
        })
        
        alert.addAction(UIAlertAction(title: "เลือกจากไฟล์", style: .default) { _ in
            self.presentDocumentPicker(completionHandler: completionHandler)
        })
        
        alert.addAction(UIAlertAction(title: "ยกเลิก", 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)
        }
    }
}

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

การเลือกไฟล์ใน WebView

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] ?? [:] // เรียกใช้งาน event บน 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("เกิดข้อผิดพลาดในการแปลงข้อความ 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] ?? [:]
            
            // เรียกใช้งาน event บน 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("เกิดข้อผิดพลาดในการแปลงข้อความ H5: \(error.localizedDescription)")
        }
    }
}

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

การแก้ไขปัญหา

1. WebView โหลดหน้าเว็บไม่สำเร็จ

  • ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต
  • ตรวจสอบการตั้งค่า NSAppTransportSecurity ใน Info.plist
  • ตรวจสอบว่า URL ถูกต้องและเข้าถึงได้

2. การขอสิทธิ์ถูกปฏิเสธ

  • ตรวจสอบว่าเพิ่ม key อธิบายการใช้งานสิทธิ์ใน Info.plist ครบถ้วน
  • แนะนำผู้ใช้ให้เปิดสิทธิ์เองที่ การตั้งค่า > ความเป็นส่วนตัว หากปฏิเสธสิทธิ์

3. การเชื่อมต่อ JavaScript Bridge มีปัญหา

  • ตรวจสอบว่า WebViewBridge ลงทะเบียน interface กับ WKUserContentController แล้ว
  • ตรวจสอบ payload จาก JavaScript ว่าตรงกับ schema JSON ที่กำหนด
  • ใช้ Safari Web Inspector (เมนู Develop) สำหรับ debug JavaScript ใน WKWebView

4. ปัญหาอัปโหลดไฟล์

  • บน iOS 13 และต่ำกว่า ให้ใช้ UTI ที่ถูกต้อง (เช่น public.image, public.data) เมื่อเรียก document picker
  • ตรวจสอบว่าแอปมีสิทธิ์เข้าถึงรูปภาพและไฟล์
  • เก็บ URL ของไฟล์ที่เลือกไว้จนกว่าอัปโหลดจะเสร็จสิ้น และอย่าเข้าถึง security-scoped URL ที่หมดอายุ