Introduction
I have found one use case where NSViewRepresentable breaks completely, stops responding and the worst of all: does not raise any error, exception, nor warning.
The exact same use case works as expected using UIViewRepresentable.
Context
The app
Web is a Browser for iOS
and macOS
, built on top of SwiftUI
to empower a better User Experience, and higher Usability.
Frameworks
SwiftUI
is not enough to build a web browser, it has its limitations, the hardest one for being that WebKit
is not supported yet, and very likely it will never be, or at least not soon; this is just my uneducated, and potentially inaccurate prediction after having worked with WebKit
for several years.
WebKit
Historically, any web browser for iOS
needed to be built using WebKit
and WKWebView
. Those were the rules, and in my opinion, good ones, as they ensure anyone downloading a browser on iOS
would get as much security as possible; but the rules changed recently. In any case, I still believe in WebKit
and for me there is no better alternative.
On macOS
there was always the possibility to use a different browser engine, but considering most web browsers are NOT even on the Mac App Store, and NONE of the mainstream ones are, I consider it an statement to publish a browser built using WebKit
and supporting most of the functionality that a mainstream browser does.
A world in itself
WebKit
is infamous for its complexity, there are many things happening there and we, as third party developers, have minimum access to them. We don’t have control over everything, we can’t configure out many things, and there are always processes running on the background that we can’t even observe. Folks could base their PhD dissertation on the art of debugging WKWebView
.
WebKit
is one of the lowest level frameworks on Apple ecosystems, and also one of the hardest to work with, even using UIKit
(on AppKit
being even harder).
The bridge
WKWebView
is a subclass of UIView/NSView
, to use it on SwiftUI
we need to do it through UIViewRepresentable
, NSViewRepresentable
or NSViewControllerRepresentable. On this case made sense to use NSViewRepresentable,
as we don’t need a NSViewController
.
Browser tabs
Each tab on the browser is represented by a different WKWebView
, displayed using one of the representable alternatives. When switching to a different tab the WKWebView
is removed from being displayed, but it keeps being retained in memory, it will be added to a representable again when it is time to show it once more.
This had been working as expected for years, until…
Bug
On macOS
, while visiting Reuters, then switching tabs, and coming back to Reuters, the screen becomes blank, completely empty. No error thrown, no exception, no warning, all the other tabs keep working.
Exploration
Debugged the issue for a whole week, unable to get any information, or to prevent the bug from keep happening. Reproducible 100%
of the time, but only for that website.
WKWebView
just becomes completely irresponsible, the NSView
properties can be accessed, for example frame
and isHidden
, needsLayout
, draw(dirtyRect:)
, etc. And nothing seems to work to bring back the web view to work.
evaluateJavaScript(_:completionHandler:)
doesn’t work anymore, reload()
, nor any other of WKWebView
’s methods respond either.
Inspecting
The JavaScript
console stops responding, there is no error or exception shown, it just goes blank and shows an empty screen with empty content.
Hypothesis
I have only seen it happening on macOS
, on iOS
this works as expected.
It only seems to happen on reuters.com so far, but my theory is that this will happen on other websites, maybe it is due to a specific web library they use, maybe some UI/React
framework, maybe some other dependency.
If it happens in reuters.com I can safely assume it will also happen on many other websites, but I just haven’t been lucky enough to stumble onto them.
To make it clear, for sure it is an issue on my browser, and not on reuters.com
Solution
Replacing NSViewRepresentable
by NSViewControllerRepresentable
fixed the issue. I’m not exactly sure how or why, but this issue is not there anymore.
To make it compatible, I wrap WKWebView
in a basic NSViewController
. Such a small price to pay considering the severity of the bug it fixes.
Before
Using NSViewRepresentable
func makeNSView(context: Context) -> some NSView {
webView
}
After
Using NSViewControllerRepresentable
func makeNSViewController(context: Context) -> some NSViewController {
let controller = NSViewController()
controller.view = webView
return controller
}
Explanation
Attempting to explain it
The inner workings of SwiftUI
remain hidden to this day, and we still don’t understand many things that happen under the hood.
When we use a variant of the representables, either UIViewRepresentable
, NSViewRepresentable
or NSViewControllerRepresentable
, we have even less control and information over what is happening.
And there is, to my point of view, a discrepancy between UIViewRepresentable
and NSViewRepresentable
, such that some scenarios that are completely functional on one won’t necessarily be on the other. Hopefully, when building for macOS
we have an alternative.
Conclusion
While I believe is highly unlikely many developers will face the same issue I was facing, as seems a fairly isolated edge-case, using WKWebView
, and on just very few websites (I have only found 1 so far), I’m positive the issue could be manifested on other cases, using different subclasses of NSView
or UIView
.
If you ever find yourself struggling to understand why your NSViewRepresentable
stopped working without any explanation, try to use a NSViewControllerRepresentable
and maybe your issue will be no more.
Let me know if you have any question.