黑白梦黑白梦

toggle navtoggle nav
  • 文章
  • 专栏
  • 文章
  • 专栏

用 SwiftUI 构建 Hybrid 应用:WKWebView 封装与 Promise 风格 JSBridge

发布于 2026-01-12

在 SwiftUI 中封装 WKWebView,构建一个可用于 Hybrid 应用的最小架构,包括 JS → Native 通信、Promise 风格的 JSBridge 实现、安全区处理以及 WKWebView 的调试方法。

创建 WebView 组件

将 UIKit 的 WKWebView 包装成一个 SwiftUI View,并通过 Coordinator 建立 JS → Native 的通信桥梁。

包装 WKWebView

WKWebView 是 UIKit 组件,SwiftUI 不能直接使用 UIKit View,必须通过 UIViewRepresentable 进行桥接。

  • 通过 makeUIView 创建并返回真正的 WKWebView,负责一次性初始化(config、delegate、load)
  • 通过 makeCoordinator 创建“中间人”创建“中间人”(引用类型),是 JSBridge、导航回调、生命周期处理的核心
  • updateUIView 在 SwiftUI 状态更新时触发,对于 Hybrid WebView 通常不需要刷新页面,留空是推荐做法,避免页面重载和状态丢失
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {

    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        let config = WKWebViewConfiguration()
    
        let webView = WKWebView(frame: .zero, configuration: config)
        webView.navigationDelegate = context.coordinator
    
        webView.load(URLRequest(url: url))
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }  

}

JS -> Native 通信

在创建 WKWebView 时,通过 WKWebViewConfiguration 注册 JS 调用点,"native" 是 JS 调用的通道名:

let contentController = WKUserContentController()
contentController.add(context.coordinator, name: "native")

config.userContentController = contentController

对应 JS 侧调用方式:

window.webkit.messageHandlers.native.postMessage(data)

在 Coordinator 实现 WKScriptMessageHandler 协议,接收 JS 消息:

  • message.name:通道名(对应注册的 "native")
  • message.body:JS 传入的数据(可为字符串 / 对象 / 数组等)
  • 在这里进行原生逻辑处理或指令分发
class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {

    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        if message.name == "native" {
            print("JS message:", message.body)
        }
    }
}

使用 WebView 组件

跟其他 View 组件用法类似, url 传入网址:

WebView(
    url: URL(string: "https://example.com")!
)
.ignoresSafeArea()

安全区

ignoresSafeArea 是 SwiftUI 的布局修饰符,告诉 SwiftUI 这个 View 是“无视系统安全区域 Safe Area ”,即让 Web 容器覆盖到这些区域:

  • 刘海
  • 状态栏
  • 桌面指示器
  • 底部手势条

Safe Area 由 Web 使用 CSS env 变量自行处理:

html, body {
  height: 100%;
}

body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

env(safe-area-inset-*) 是 Web 标准(CSS), 但数值是由原生 WebView / 浏览器运行环境注入的。

查看 web 控制台

XCode 的控制台只能查看原生的 print 输出。

通过 Mac Safari 的 Develop → Web Inspector 查看。

WKWebView 需开启调试;

webView.isInspectable = true

然后打开 Mac Safari 的开发菜单,在开发菜单找到模拟器,点击WebView 页面 URL,即可打开控制台,直接可用,不需要额外设置。

原生桥

制定消息协议

通过 JSON 格式通信:

JS → Native

{
  "id": "uuid",
  "type": "getDeviceInfo",
  "payload": {}
}

Native → JS

{
  "id": "uuid",
  "success": true,
  "data": {...}
}

原生处理请求

把 webView 实例绑定到 Coordinator 中:

context.coordinator.bind(webView: webView)

解析请求的格式

class Coordinator: NSObject, WKScriptMessageHandler {

    weak var webView: WKWebView?

    func bind(webView: WKWebView) {
        self.webView = webView
    }
}

解析请求的格式:

 func userContentController(
     _ userContentController: WKUserContentController,
     didReceive message: WKScriptMessage
 ) {
     guard
         let body = message.body as? [String: Any],
         let id = body["id"] as? String,
         let type = body["type"] as? String
     else { return }
     
     handle(type: type, id: id, payload: body["payload"])
 }

处理不同 Bridge 类型:

func handle(type: String, id: String, payload: Any?) {
    switch type {
    case "getDeviceInfo":
        let data: [String: Any] = [
            "platform": "iOS",
            "version": UIDevice.current.systemVersion
        ]
        respond(id: id, success: true, data: data)

    default:
        respond(id: id, success: false, error: "Unknown type")
    }
}

回调 JS ,即执行给 window 注入的 __handleNativeResponse 方法:

func respond(
   id: String,
   success: Bool,
   data: Any? = nil,
   error: String? = nil
) {
   let response: [String: Any] = [
       "id": id,
       "success": success,
       "data": data ?? NSNull(),
       "error": error ?? NSNull()
   ]

   guard
       let jsonData = try? JSONSerialization.data(withJSONObject: response),
       let json = String(data: jsonData, encoding: .utf8)
   else { return }

   let js = "window.__handleNativeResponse(\(json))"

   webView?.evaluateJavaScript(js)
}

JS Bridge

JS 发送消息到 Native,给每个请求创建一个 id ,把回调存起来。

const callbacks = new Map<string, Callback>();

export function callNative<T = any, P = any>(type: string, payload?: P): Promise<T> {
  return new Promise((resolve, reject) => {
    const id = generateId();

    callbacks.set(id, { resolve, reject });

    const message: NativeRequest<P> = {
      id,
      type,
      payload,
    };

    if (!window.webkit || !window.webkit.messageHandlers || !window.webkit.messageHandlers.native) {
      callbacks.delete(id);
      reject(new Error("Native bridge not available"));
      return;
    }

    window.webkit.messageHandlers.native.postMessage(message);
  });
}

Native 响应后,JS 接收消息,并根据 id 拿到存起来的 resolve 或 reject 执行,实现 Promise 的效果。

window.__handleNativeResponse = function (response: NativeResponse) {
  const { id, success, data, error } = response;
  const cb = callbacks.get(id);
  if (!cb) return;

  if (success) {
    cb.resolve(data);
  } else {
    cb.reject(error);
  }
  callbacks.delete(id);
};

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com