iOS下使用WKWebView滚动时如何检测屏幕内可见的HTML元素
实现方案
整体逻辑是通过IntersectionObserver API在网页端检测文本块曝光状态,跨端通信将已读状态同步到原生层,原生层统计完成度后控制下一步操作入口:
1. 预处理静态HTML结构
你在调用loadHTMLString之前,先给需要检测的文本块统一添加标识:
- 给所有待检测的文本块(比如
<p>、<div>标签)增加类名read-track-block - 给每个文本块分配唯一ID,格式如
block-0、block-1,方便统计和去重
如果你不知道原HTML的文本块数量,也可以在JS层动态获取总数后同步给原生层。
2. 注入网页端检测逻辑
创建WKUserScript注入监听逻辑,JS代码示例如下:
// 等待DOM加载完成 document.addEventListener('DOMContentLoaded', function() { // 获取所有待检测的文本块 const trackBlocks = document.querySelectorAll('.read-track-block'); const total = trackBlocks.length; // 先同步总块数给原生层(可选,如果你预处理时已经知道总数可以省略) window.webkit.messageHandlers.readBlockHandler.postMessage({ type: 'total', count: total }); const readSet = new Set(); // 初始化曝光观察器,70%面积进入视口即判定为已读 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const blockId = entry.target.id; if (entry.intersectionRatio >= 0.7 && !readSet.has(blockId)) { readSet.add(blockId); // 上报已读块ID给原生层 window.webkit.messageHandlers.readBlockHandler.postMessage({ type: 'read', blockId: blockId }); // 可选:给已读块增加样式提示用户 entry.target.classList.add('is-read'); } }); }, { threshold: 0.7 }); // 给所有文本块绑定观察器 trackBlocks.forEach(block => { observer.observe(block); }); });
3. Swift端配置WKWebView
初始化时配置消息监听和脚本注入:
// 1. 初始化配置 let config = WKWebViewConfiguration() let userContent = WKUserContentController() // 2. 添加消息监听处理器,名称要和JS端一致 userContent.add(self, name: "readBlockHandler") // 3. 注入上述JS脚本,页面加载完成后注入 if let jsPath = Bundle.main.path(forResource: "readTrack", ofType: "js"), let jsContent = try? String(contentsOfFile: jsPath) { let userScript = WKUserScript(source: jsContent, injectionTime: .atDocumentEnd, forMainFrameOnly: true) userContent.addUserScript(userScript) } config.userContentController = userContent // 4. 初始化WKWebView let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = self
4. Swift端处理已读统计
实现WKScriptMessageHandler代理方法处理跨端消息:
// 存储总文本块数和已读ID集合 private var totalBlocks: Int = 0 private var readBlockIds: Set<String> = [] func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "readBlockHandler", let body = message.body as? [String: Any], let type = body["type"] as? String else { return } switch type { case "total": // 接收JS上报的总块数 totalBlocks = body["count"] as? Int ?? 0 case "read": // 接收已读块ID,去重存储 guard let blockId = body["blockId"] as? String else { return } readBlockIds.insert(blockId) // 判断是否全部已读 if readBlockIds.count == totalBlocks && totalBlocks > 0 { // 全部已读,解锁下一步按钮 nextButton.isEnabled = true } default: break } }
注意事项
- 避免重复上报:JS层和原生层都做了去重逻辑,不用处理同一块多次触发的问题
- 长文本适配:如果单块文本高度超过屏幕高度,可以把
IntersectionObserver的threshold调整为多个阈值,比如[0.2, 0.5, 0.8],保证用户确实浏览了大部分内容 - 内存管理:页面销毁时要移除消息处理器,避免内存泄漏:
webView.configuration.userContentController.removeScriptMessageHandler(forName: "readBlockHandler")
内容的提问来源于stack exchange,提问作者ymotov
相关产品推荐
相关产品推荐

