You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI监听WKWebView的estimatedProgress导致视图无限更新如何解决?

问题根本原因

你陷入无限更新的核心是两个问题叠加:

  1. updateUIView 方法里没有判断userSetURL是否真的发生变化,每次WebView重绘就直接调用load重新加载页面
  2. 每次页面重新加载都会触发estimatedProgress变化,你把这个值通过@Binding回写给父视图的@State,父视图@State变化就会触发body重绘,进而再次调用WebView的updateUIView,再次加载页面,形成死循环
修复方案

第一步:给updateUIView加变化判断,只有userSetURL真的变了才重新加载

首先在Coordinator里存储上次加载的URL和WebView弱引用,用来做对比和KVO清理:

class Coordinator: NSObject, WKNavigationDelegate {
    var parent: WebView
    var webViewNavigationSubscriber: AnyCancellable? = nil
    var lastLoadedURL: URL?
    weak var webView: WKWebView? // 存储WebView引用用于后续移除观察者
    
    init(_ webView: WebView) {
        self.parent = webView
    }
    
    deinit {
        webViewNavigationSubscriber?.cancel()
        // 页面销毁时移除KVO观察者避免崩溃
        webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
    }
    // 其他原有方法保持不变
}

修改updateUIView和load方法,增加重复加载拦截:

func updateUIView(_ webView: WKWebView, context: Context) {
    // 只有当userSetURL和上次加载的不一致时才触发加载
    if userSetURL != context.coordinator.lastLoadedURL {
        load(url: userSetURL, in: webView)
        context.coordinator.lastLoadedURL = userSetURL
    }
}

fileprivate func load(url:URL?, in webView:WKWebView) {
    let targetURL = url ?? defaultURL
    // 重复加载相同URL直接跳过
    guard webView.url != targetURL else { return }
    let req = URLRequest(url: targetURL)
    webView.load(req)
}

同时在makeUIView方法返回前给Coordinator存储WebView引用:

func makeUIView(context: Context) -> WKWebView {
    let configuration = WKWebViewConfiguration()
    let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
    webView.navigationDelegate = context.coordinator
    webView.allowsBackForwardNavigationGestures = true
    webView.scrollView.isScrollEnabled = true
    webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
    // 给Coordinator存储WebView引用
    context.coordinator.webView = webView
    load(url: userSetURL, in: webView)
    context.coordinator.lastLoadedURL = userSetURL
    return webView
}

第二步:优化estimatedProgress的更新逻辑,避免不必要的状态刷新

KVO回调里不需要额外包装主队列异步派发,WKWebView的estimatedProgress KVO回调本身就运行在主队列,同时增加值变化判断,避免重复赋值触发刷新:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    dispatchPrecondition(condition: .onQueue(.main))
    guard let wv = object as? WKWebView, keyPath == #keyPath(WKWebView.estimatedProgress) else { return }
    // 只有值真的变了才更新Binding
    if parent.estimatedProgress != wv.estimatedProgress {
        parent.estimatedProgress = wv.estimatedProgress
    }
}

第三步:优化父视图状态类型,减少不必要重绘

把不需要可选的状态去掉可选修饰,减少可选解包带来的不必要视图结构变化:

struct TestWContainer: View {
    @State var url:URL?
    @State var userSetUrl:URL?
    @State var showLoader:Bool = false
    @State var estimatedProgress:Double = 0
    
    var body: some View {
        ZStack {
            WebView(currentURL: $url, userSetURL: $userSetUrl, showLoader: $showLoader, estimatedProgress: $estimatedProgress)
            
            if estimatedProgress > 0 && estimatedProgress < 1 {
                VStack(spacing:0) {
                    ProgressView(value: estimatedProgress, total: 1)
                        .frame(height: 3)
                    Spacer()
                }
            }
        }
    }
}

对应WebView里的showLoader和estimatedProgress的@Binding也同步去掉可选修饰即可。

此类场景通用设计规范
  • UIViewRepresentable的updateUIView方法必须加判断逻辑,只有相关依赖真的发生变化时才操作UIView,不要每次执行都做重加载、修改属性等高成本操作
  • 从UIKit侧回传给SwiftUI的Binding值,要先判断和当前值是否相同,相同就不要重复赋值,避免触发不必要的SwiftUI视图重绘
  • 尽量减少视图状态的可选类型,除非业务逻辑真的需要,默认值能避免很多不必要的视图结构变化

内容的提问来源于stack exchange,提问作者zumzum

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 12:45:05