跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Dart

iOS 18.2 上 Flutter WebView 点击失效的来龙去脉

iOS 18.2 上 Flutter WebView 出现点击失效,根源在于 WKWebView 内部手势识别器与 Flutter 的 DelayingGestureRecognizer 状态冲突。临时移除再添加 recognizer 的绕过方式在正式版中导致触摸穿透回归,被条件性回退。社区通过 PointerInterceptor 规避,官方正推进用 HitTest 策略替代延迟识别器,未来依赖 FFI 同步查询手势竞技场,彻底解决平台视图手势问题。

RustyLab发布于 2026/6/30更新于 2026/10/760 浏览
iOS 18.2 上 Flutter WebView 点击失效的来龙去脉

前几天 #175099 又报了一个 iOS 18.2 上的问题:webview_flutter 里的点击偶尔失灵,点不动或者点了没反应。根子还是 WKWebView 内部的手势识别器和 Flutter Engine 里那个用来阻止/延迟手势的 recognizer 产生了冲突。

去年 iOS 18.2 beta 就闹过一回。当时 Engine 那边通过 #56804 搞了一个临时绕过——把 delayingRecognizer 先移除再加回来,利用这个操作刷新 WebKit 的内部状态,让点击恢复。但这个补丁在 iOS 18.2 正式版上造成了更麻烦的回归:overlay 的手势阻止会失效,触摸直接穿透到下面的 WebView。所以 Flutter 团队在 iOS 18.2 条件下又把那个提交回退了。

文章配图

Flutter 那边也确认了这是 Apple / WebKit 自己的 bug,已经同步上报,两边在协作修复。

到底哪里不对

最早是在 iOS 18.2 beta 上发现的:页面上先触发过某个 Flutter widget 或 overlay(比如 context menu、Drawer),然后 WKWebView 里的链接、按钮就点不动了——能看到高亮,但不会跳转。重新加载 WebView 能恢复。

原因得从 Flutter 在 iOS 上展示 PlatformView 的那套手势机制说起。Flutter 会在承载 WKWebView 的视图上加一个 FlutterDelayingGestureRecognizer(delayingRecognizer),通过切换它的状态(possible、ended、failed)来告诉 UIKit 的其他 recognizer 该不该拦手势。

https://developer.apple.com/documentation/uikit/about-the-gesture-recognizer-state-machine

稍微啰嗦一句背景:Flutter 和 iOS 各自有一套手势识别系统。把一个原生控件(比如 WKWebView)嵌进 Flutter 时,层级大概是这样的:

[FlutterView] ← 整个 Flutter 渲染层
├─ Flutter widgets
│   ↑
│   │
│   手势由 Flutter framework(Dart)处理
│   └─ PlatformView (e.g. WKWebView)
│       ↑
│       手势由 UIKit / WebKit 内部 recognizer 处理

为了防止互相抢事件,Flutter Engine 在 iOS 上引入了一个'延迟识别器'(delaying gesture recognizer)。简单说就是:当 Flutter 检测到某个 widget 要阻止事件(比如 GestureDetector 或者 overlay 遮罩),它就通过这个 delayingRecognizer 让 UIKit 里的 recognizer(包括 WKWebView 的点击识别器)暂停响应。

这套机制在 Flutter 和 UIKit 手势交界处非常敏感。这次的 bug 就是:WKWebView 内部的某些 recognizer 会'缓存'或持有 的旧状态。Flutter 在运行时切换 状态(比如 )时,WebKit 的部分识别器拿到的是过期状态,结果就是只高亮不执行动作。

delayingRecognizer
delayingRecognizer
blockGesture

临时方案与回归

在 iOS 18.2 上,Flutter 团队试过好几种办法:toggle enabled、塞入 dummy recognizer、异步 dispatch、重建 recognizer 实例……最后发现把同一个 delayingRecognizer 实例移除再添加回去,能迫使 UIKit 刷新相关 recognizer 的关联,WebKit 内部识别器就能看到最新状态,点击功能恢复。

这个'移除再添加'的逻辑大概在 Flutter 3.29 里发布了。

但 iOS 18.2 高版本上,这个做法又捅了另一个篓子:Drawer、overlay 等需要阻止手势的场景完全失效了,触摸会穿透到下面的 WebView。

文章配图

这比点不动还严重,直接导致误触和功能错乱。

于是 Flutter 在代码里加了条件判断,针对 iOS 18.2(@available(iOS 18.2.0, *))不再执行那个'移除再添加'的绕过。可这样一来,之前靠它修好的'WebView 点不动'就又回来了。

很明显,这就是 iOS 18.2 上 WKWebView 自带的毛病,而且系统升级导致 WebKit 内部 recognizer 缓存行为变了。要彻底修好,还是得 Apple 出手。

文章配图

怎么踩中这个坑

触发条件挺明确的:只要 WebView 上面出现过需要拦截触摸的 overlay 或 widget,就有可能中招。比如:

  • 弹出一个半透明 ModalBarrier
  • 打开一个 Drawer
  • 显示一个 PopupMenu
  • 调用 showDialog()
  • 某些动画(Hero)内部也会临时创建 overlay 层

这些操作都会让 Engine 调用 delaying_recognizer.blockGesture(true),WKWebView 内部的 recognizer 暂停响应。overlay 消失后,Engine 再调 blockGesture(false),但 UIKit 没把 WKWebView recognizer 的响应恢复过来,问题就出现了。

高版本上那个移除再添加的做法,不仅刷新了依赖关系,还重置了某些全局 recognizer 的 delaysTouchesBegan / requiresFailureOf 配置——这些配置正好是 Flutter Engine 用来防止 overlay 点击穿透的。所以补丁一打,防穿透的逻辑就废了。

社区临时解决办法

目前社区里比较通用的规避方案是用 pointer_interceptor 来隔离 overlay 和 WebView 的事件竞争。核心思路是:在 iOS 上,只要 WebView 上方有视图并且发生过交互,WebView 就可能停止接受点击。所以用 PointerInterceptor 可以防止在和 WebView 上方的视图交互后,WebView 的点击中断。

// 判断当前是否在导航栈顶部
bool get _isTopOfNavigationStack => ModalRoute.of(context)?.isCurrent ?? false;

// 包裹 WebView 的 Widget
Widget buildWebviewWithIOSWorkaround(BuildContext context) {
  return Stack(
    children: [
      buildWebView(context),
      if (Platform.isIOS)
        Positioned.fill(
          child: PointerInterceptor(
            intercepting: !_isTopOfNavigationStack,
            // WebView 不在顶部 → 阻止点击穿透
            debug: false,
            child: const SizedBox.expand(),
          ),
        ),
    ],
  );
}

官方在做什么

一边和 Apple 推修复,一边也在想办法从外部绕过去。其中一个尝试是通过全新的 HitTest 策略来规避。

文章配图

#176597 这个 PR 的大致想法是:既然大部分用例里平台视图只有一个重叠区域,那如果触摸位置落在 Flutter Widget 和平台视图之间的'重叠'范围里,就阻止平台视图上的所有 UIGestureRecognizer。具体做法:

  • 新增一个拦截策略枚举 FlutterPlatformViewGestureRecognizersBlockingPolicyHitTestByOverlay
  • 在 platform view 的触摸/hitTest 逻辑里,如果某个点落在 overlay 区域,就让 hitTest: 返回自己(self),而不是继续往底层 WebView 传
  • 对于这种策略,blockGesture 方法变成空操作,因为拦截已经在 hitTest 层面做掉了
  • 在 controller 更新 overlay 层时,把 overlay 视图引用记录下来,交给内部的 intercepting view,这样拦截逻辑就有 overlay 区域信息可用

也就是说,这套方案不依赖 delaying recognizer 的状态切换,而是用 hitTest 直接屏蔽底层点击。

但有个小缺点:如果多个重叠区域被合并成一个触摸阻挡区域,那个区域会是包含所有重叠的最小包围框。

文章配图

做了一半,维护人员发现也许完整方案并不复杂,于是决定关掉这个临时 MVP:

文章配图

文章配图

完整方案依赖于 FFI 从 Flutter 手势竞技场同步查询来做决策,这又是另一个大改动。

所以现在推进到了 #177859。这个 PR 彻底放弃'延迟识别器'来阻塞 platform view 手势,改为在 iOS 端对触点做同步 hit-test,利用 FFI 从 framework 查询该手势是否应该被接受或阻止。它解决的就是 web_view、admob 这些平台视图点不动的问题,同时新增一个可选的 blocking policy(FlutterPlatformViewGestureRecognizersBlockingPolicyHitTest)。

调整细节:

  • 把识别器从 delaying 类型改成直接 hit test 判断是否应阻止手势——根据触点是否落在应该被 platform view 拦截的区域来决定
  • 用 FFI 在 native 层同步调用 framework 里的函数(_platformViewShouldAcceptGesture / platformViewShouldAcceptGesture),避免主线程互相等待死锁
  • 新增 FlutterPlatformViewGestureRecognizersBlockingPolicyHitTest 策略,逐步采纳,不直接替换旧策略,降低回归风险

PR 涉及 engine 的 UI 层、iOS 平台 view / embedder 相关代码,把 hit-test 入口函数、FFI 绑定和 platform view 手势决策路径都串起来了。

这是个高风险改动,尤其是会动到 iOS 平台视图(platform view)的手势处理路径,影响 web_view、admob 和任何嵌入 UIView 的插件。官方建议插件作者(特别是官方 1P 插件)后续切换到新 policy。

当然 PR 还没合,眼下我们能做的规避还有:

  • 别在 WebView 上方放需要拦截手势的复杂 overlay,尽量减少交互型 overlay;能避免覆盖 WebView 就不会触发
  • 在 overlay 关闭后重建 WebView(重建 controller / reload),但闪烁体验不好

所以目前看来,路线是先完成 #176597,再实现 FFI 同步查询的完整方案。版本情况大概是:

  • iOS 18.2 上因重叠控件导致 WebView 点击失效的问题,需要 3.29 及以上
  • 在 3.35.4 之前,会出现触摸穿透;3.35.4 之后由于 cherry-pick revert,又退回点击无效
  • 以上两个问题都可以用 pointer_interceptor 尽量规避
  • 等官方内置 HitTest + FFI 方案发布

之前那次线程合并为 FFI 提供了基础,也给这次调整指了条新路,只是改动确实需要更谨慎一些。

参考链接

  • https://github.com/flutter/flutter/pull/176597
  • https://github.com/flutter/flutter/issues/175099
  • https://github.com/flutter/flutter/pull/177859

目录

  1. 到底哪里不对
  2. 临时方案与回归
  3. 怎么踩中这个坑
  4. 社区临时解决办法
  5. 官方在做什么
  6. 参考链接

更多推荐文章

查看全部
  • AI 安全:Stable Diffusion 视觉提示词注入攻击原理与实现
  • 程序员 LLM 学习指南:从入门到进阶的技术路线
  • JS 生成 UUID 的常见方案与生产环境避坑指南
  • Linux Ext 系列文件系统详解
  • 面向新手的鸿蒙跨平台开发技术选型指南
  • LangChain4j 集成国产大模型(通义千问、文心一言、智谱 AI)详解
  • 民用无人机新规 2026 年 5 月实施 实名登记与激活双重要求
  • Xcode 真机调试报错:Developer Disk Image 无法卸载解决方案
  • Spring AI MCP Server 集成与源码解析
  • Arduino BLDC 使用 6.5 寸轮毂电机的智能动态跟随机器人底盘
  • Rust 与 WebAssembly 深度实战:在浏览器与 Node.js 中运行高性能代码
  • SimPO 大模型对齐算法原理与 ms-swift 实践
  • Python 入门基础教程:从基础知识到项目实战指南
  • C++ 函数重载:核心规则、实现细节与实战
  • 多模态 AI 应用:图文音视频一体化开发实战
  • OpenClaw QQ 机器人接入指南
  • Gitee 代码上传实战:Git 基础与远程仓库配置指南
  • 二分查找:山峰数组的峰顶索引与寻找峰值
  • 大模型转行指南:四大方向解析与入行建议
  • GitHub Copilot 人工智能编程助手功能介绍

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online