SwiftUI监听WKWebView的estimatedProgress导致视图无限更新如何解决?
问题根本原因
你陷入无限更新的核心是两个问题叠加:
updateUIView方法里没有判断userSetURL是否真的发生变化,每次WebView重绘就直接调用load重新加载页面- 每次页面重新加载都会触发
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
相关产品推荐
相关产品推荐

