SwiftUI WKWebView 相机集成完整指南

ai可以随意生成很多高质量内容,但是同一个问题每次看生成的新的结果未免太累,熟悉的内容多次重复倒是不错的方式,这也是为什么当下我记录的理由,当然写点东西一直是学一个语言最直接高级的方式,因为难度最大效果才最好,坚持喽
本文由 Cursor 生成,本文介绍如何在 SwiftUI 中使用 WKWebView 实现 H5 与原生相机的交互,支持远程 URL 和本地 HTML 两种加载方式。

ps近期项目的一个问题刚好,native和angular结合的hyperapp,但是呢拍照的js-native交互

  1. 之前没有用js-native交互,用的input标签拍照,最后测试发现iOS17基本各个版本都不能用,一调起相机整个web就重新刷新;
  2. 改成交互好多了,但是独独iOS17.7.1有之前的问题
  3. 后面客户那边拿了真机过来,自己写了iOS版的测试,竟然没问题,看来是他们壳的问题

这个项目总结下来两个奇怪的问题

  1. 上面的iOS17.7.1问题
  2. iOS上的input标签响应聚焦问题,根本不能控制

📋 目录


🎯 核心功能

实现 H5 页面与 iOS 原生相机的无缝交互:

  1. H5 → Native:网页调用原生相机
  2. Native → H5:拍照后将 base64 图片数据回传给网页
  3. 双向通信:使用 WKScriptMessageHandlerevaluateJavaScript

🔧 完整实现

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: 检查以下几点:

  1. WKUserContentController 是否正确注册了 takePhoto
  2. H5 中的调用是否正确:window.webkit.messageHandlers.takePhoto.postMessage(null)
  3. 查看 Safari 开发者工具的控制台错误

Q3: 如何调试 H5 页面?

A:

  1. 真机连接 Mac
  2. Safari → 开发 → [你的设备] → [你的 WebView]
  3. 可以实时查看控制台和网络请求

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