คู่มือการเชื่อมต่อ 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 ที่หมดอายุ
