SwiftUI WKWebView 相机集成完整指南
ai可以随意生成很多高质量内容,但是同一个问题每次看生成的新的结果未免太累,熟悉的内容多次重复倒是不错的方式,这也是为什么当下我记录的理由,当然写点东西一直是学一个语言最直接高级的方式,因为难度最大效果才最好,坚持喽
本文由 Cursor 生成,本文介绍如何在 SwiftUI 中使用 WKWebView 实现 H5 与原生相机的交互,支持远程 URL 和本地 HTML 两种加载方式。
ps近期项目的一个问题刚好,native和angular结合的hyperapp,但是呢拍照的js-native交互
- 之前没有用js-native交互,用的input标签拍照,最后测试发现iOS17基本各个版本都不能用,一调起相机整个web就重新刷新;
- 改成交互好多了,但是独独iOS17.7.1有之前的问题
- 后面客户那边拿了真机过来,自己写了iOS版的测试,竟然没问题,看来是他们壳的问题
这个项目总结下来两个奇怪的问题
- 上面的iOS17.7.1问题
- iOS上的input标签响应聚焦问题,根本不能控制
📋 目录
🎯 核心功能
实现 H5 页面与 iOS 原生相机的无缝交互:
- H5 → Native:网页调用原生相机
- Native → H5:拍照后将 base64 图片数据回传给网页
- 双向通信:使用
WKScriptMessageHandler和evaluateJavaScript
🔧 完整实现
1️⃣ SwiftUI 入口
import SwiftUI
@main
struct WebCameraApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
WebContainerView()
.ignoresSafeArea()
}
}
2️⃣ UIViewControllerRepresentable 包装器
import SwiftUI
import WebKit
struct WebContainerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> WebViewController {
WebViewController()
}
func updateUIViewController(_ uiViewController: WebViewController, context: Context) {}
}
3️⃣ WebViewController 核心实现
import UIKit
import WebKit
class WebViewController: UIViewController,
WKScriptMessageHandler,
UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
var webView: WKWebView!
var imagePicker: UIImagePickerController?
override func viewDidLoad() {
super.viewDidLoad()
// 1. 配置 WKWebView
let contentController = WKUserContentController()
contentController.add(self, name: "takePhoto") // JS 调用入口
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: config)
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
// 2. 加载网页(见下方两种方式)
loadWebContent()
}
deinit {
webView.configuration.userContentController.removeScriptMessageHandler(forName: "takePhoto")
}
// 3. JS → Native:接收拍照请求
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if message.name == "takePhoto" {
openCamera()
}
}
// 4. 打开相机
func openCamera() {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
print("相机不可用,请使用真机测试")
return
}
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = self
picker.allowsEditing = false
imagePicker = picker
present(picker, animated: true)
}
// 5. 拍照完成:转 base64 并回传给 JS
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
guard let image = info[.originalImage] as? UIImage,
let data = image.jpegData(compressionQuality: 0.8) else { return }
let base64 = data.base64EncodedString()
// 转义特殊字符
let escapedBase64 = base64
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
// Native → JS
let js = "window.onNativePhoto && window.onNativePhoto('\(escapedBase64)');"
webView.evaluateJavaScript(js) { _, error in
if let error = error {
print("JS 调用失败:", error)
}
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
// ⭐️ 加载网页内容(根据需求选择方式)
func loadWebContent() {
// 方式选择:见下一节
}
}
🔀 两种加载方式
方式 1:加载远程 URL(推荐)
适用场景:H5 页面独立部署,前后端分离
func loadWebContent() {
// ⭐️ 加载远程 URL
if let url = URL(string: "https://你的网站.com/camera-demo.html") {
webView.load(URLRequest(url: url))
}
}
优点:
- ✅ H5 代码独立维护,更新无需重新打包 App
- ✅ 支持热更新
- ✅ 前端可独立调试
方式 2:加载本地 HTML 字符串
适用场景:简单页面、离线功能、快速原型
func loadWebContent() {
// ⭐️ 加载本地 HTML 字符串
webView.loadHTMLString(htmlString, baseURL: nil)
}
var htmlString: String {
"""
<!DOCTYPE html>
<html>
<body>
<h3>SwiftUI + WKWebView 拍照 demo</h3>
<button onclick="takePhoto()">拍照</button>
<p>预览:</p>
<img id="preview" style="max-width: 100%; border: 1px solid #ccc;" />
<script>
// JS 发起拍照
function takePhoto() {
if (window.webkit?.messageHandlers?.takePhoto) {
window.webkit.messageHandlers.takePhoto.postMessage(null);
} else {
alert('native 通道不可用');
}
}
// 接收 base64 图片
window.onNativePhoto = function(base64) {
document.getElementById('preview').src = 'data:image/jpeg;base64,' + base64;
console.log('收到 base64 长度:', base64.length);
}
</script>
</body>
</html>
"""
}
优点:
- ✅ 无需网络请求
- ✅ 适合简单页面
- ✅ 调试方便
🌐 H5 端实现
如果使用方式 1(远程 URL),你的网页需要包含以下 JS 代码:
<!DOCTYPE html>
<html>
<body>
<button onclick="takePhoto()">拍照</button>
<img id="preview" style="max-width: 100%;" />
<script>
// 1. 调用原生相机
function takePhoto() {
window.webkit?.messageHandlers?.takePhoto?.postMessage(null);
}
// 2. 接收原生回传的 base64
window.onNativePhoto = function(base64) {
document.getElementById('preview').src = 'data:image/jpeg;base64,' + base64;
}
</script>
</body>
</html>
TypeScript 类型声明(可选)
declare global {
interface Window {
webkit?: {
messageHandlers?: {
takePhoto?: {
postMessage: (data: any) => void;
};
};
};
onNativePhoto?: (base64: string) => void;
}
}
🔐 权限配置
在 Info.plist 中添加相机权限:
<key>NSCameraUsageDescription</key>
<string>需要使用相机拍照并回传给网页</string>
🔄 交互流程
┌─────────────────────────────────────────────────────┐
│ 完整交互流程 │
└─────────────────────────────────────────────────────┘
H5 端 Native 端
│ │
│ 1. 用户点击"拍照"按钮 │
├──────────────────────────────>│
│ window.webkit.messageHandlers│
│ .takePhoto.postMessage(null) │
│ │
│ 2. 收到请求
│ openCamera()
│ │
│ 3. 打开相机
│ UIImagePicker
│ │
│ 4. 用户拍照
│ │
│ 5. 转 base64
│<──────────────────────────────┤
│ window.onNativePhoto(base64) │
│ │
│ 6. 显示预览 │
│ img.src = 'data:image/...' │
│ │
📝 常见问题
Q1: 相机在模拟器上无法使用?
A: 相机功能必须在真机上测试,模拟器不支持。
Q2: JS 调用没有反应?
A: 检查以下几点:
WKUserContentController是否正确注册了takePhoto- H5 中的调用是否正确:
window.webkit.messageHandlers.takePhoto.postMessage(null) - 查看 Safari 开发者工具的控制台错误
Q3: 如何调试 H5 页面?
A:
- 真机连接 Mac
- Safari → 开发 → [你的设备] → [你的 WebView]
- 可以实时查看控制台和网络请求
Q4: 需要支持多个 JS Handler?
A: 在配置时添加多个:
contentController.add(self, name: "takePhoto")
contentController.add(self, name: "selectImage")
contentController.add(self, name: "saveData")
🎯 总结
| 特性 | 远程 URL | 本地 HTML |
|---|---|---|
| 更新方式 | 热更新 | 需重新打包 |
| 网络依赖 | 需要 | 不需要 |
| 适用场景 | 生产环境 | 原型/Demo |
| 推荐度 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
推荐方案:
- 🚀 生产环境:使用远程 URL(方式 1)
- 🧪 快速原型:使用本地 HTML(方式 2)
📚 扩展阅读
如果你需要实现以下功能:
- ✨ 加载本地 HTML 文件(Bundle)
- 🔒 HTTPS 证书忽略
- 📮 POST 表单提交
- 🎨 SwiftUI 直接封装 WKWebView(无 UIViewController)
- 📸 支持相册选择(不只是相机)
可以在此基础上进行扩展,核心思路保持不变。
完整代码已测试可运行 ✅
Happy Coding! 🎉