Memory leak in WebKit caused by KVO and @StateObject

Hi!

My SwiftUI app is a rather complex browser app. Starting with iOS 18, the app crashes due to repeted reloads of the WkWebView. I’ve tracked the issue as far as I can, but I still haven’t found the root cause.

My app is structured like this:

MainView holds a cuple of subviews. It also holds a @StateObject called viewModel that holds a lot of @Published vars. The viewModel is passed as a enivormentObject.

Example from ViewModel:

@MainActor class ViewModel: NSObject, ObservableObject {
@Published public var isLoading: Bool = false
@Published public var loadProgress: Double? = 0

public func setIsLoading(_ value: Bool) async {
            self.isLoading = value
        }
        public func setLoadProgress(_ value: Double?) async {
            self.loadProgress = value
        }
}

WebView is a subview of MainView, which holds a navigation bar, and a UIViewRepresentable, which is a WkWebView.

The WkWebView pushes some states to the ViewModel as the underlying values of the WkWebView changes, i.e. estimaedProgress, and isLoading. This is done via KVO and works like this:

estimatedProgressObservation = self.parent.webView.observe(\.estimatedProgress) { webView, progress in
                Task {
                    await parent.viewModel.setLoadProgress(webView.estimatedProgress)
                }
            }
            isLoadingObservation = self.parent.webView.observe(\.isLoading) { webView, value in
                Task {
                    await parent.viewModel.setIsLoading(webView.isLoading)
                }
            } 

By using a timer in WkWebViews Coordinator, i trigger a load after a configurable amount of time :


        func loadUrl(url: URL) {
            
            DispatchQueue.main.async {
                
                console.info("Load URL: ...", sensitive: "Load URL: \(url.absoluteString)")
                
                let policy: NSURLRequest.CachePolicy
                if self.parent.settings.noCache {
                    policy = .reloadIgnoringLocalAndRemoteCacheData
                } else {
                    policy = .useProtocolCachePolicy
                }
                
                let request = URLRequest(url: url, cachePolicy: policy)
                self.parent.webView.load(request)
            }
            
        }

Running the app with the automatic reload enabled freezes the app after a couple of hours. It also seems to freeze Safari on the device. The device needs to be rebooted.

If I inspect the device's running processes, hundreds of ”com.apple.webkit. web content " processes are running.

Removing await parent.viewModel.setLoadProgress(webView.estimatedProgress) and await parent.viewModel.setIsLoading(webView.isLoading) fixes the issue, but it is necessary for other app functions. Therefore, is suspect that the viewModel somehow causes the bug.

The issue arises after a couple of loads 5-10. The debugger shows a message when the processes start to pile up. I suspect its related.

Failed to terminate process: Error Domain=com.apple.extensionKit.errorDomain Code=18 "(null)" UserInfo={NSUnderlyingError=0x12d0e7f60 {Error Domain=RBSServiceErrorDomain Code=1 "Client not entitled" UserInfo={RBSEntitlement=com.apple.runningboard.terminateprocess, NSLocalizedFailureReason=Client not entitled, RBSPermanent=true}}}

How can I find out what causes the suspected memory leak? Instruments gives me nothing of value. The memory leak wasn't present in iOS 17. Is this a bug in iOS 18, or was something intentionally changed?

Memory leak in WebKit caused by KVO and @StateObject
 
 
Q