I am creating an app where the primary UI is a WKWebView and SwiftUI is only used for odds and ends of the app. I am running into a snag using the SwiftUI system UI TabView
. If you run this sample app there is some behavior I cannot explain
enum Page: String, Hashable, CaseIterable, Identifiable {
case settings = "gear"
case radio = "radio"
case connect = "dot.radiowaves.right"
var id: String { rawValue }
}
struct RandomColorHostedWebView: UIViewRepresentable {
let webView: WKWebView
init() {
// Clear cache to be extra sure https://stackoverflow.com/a/34376943/3833632
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ })
let webConfiguration = WKWebViewConfiguration()
self.webView = WKWebView(frame: .zero, configuration: webConfiguration)
let urlRequest = URLRequest(url: URL(string: "https://randomcolour.com")!)
self.webView.load(urlRequest)
}
func makeUIView(context: Context) -> UIView {
return webView
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
struct ContentView: View {
let pages = Page.allCases
let randomColor: RandomColorHostedWebView
init() {
randomColor = RandomColorHostedWebView()
}
var body: some View {
TabView {
ForEach(pages) { page in
randomColor
.tag(page)
.tabItem {
Label(page.rawValue, systemImage: page.rawValue)
}
}
}
}
}
If you run this you will notice that each tab you click causes a refresh animation but it does not load a new color. In addition when you go back to a tab you have already been to it will be blank.
This is a behavior I cannot explain. (I would love to understand why this happens)
That being said I am assuming the cause is that SwiftUI has no idea that each of the tabItem parent views are the same view as they have different structural identities.
So I am left looking for a workaround to either make the background of the TabView clear so I can use a ZStack
, limit the TabView
height to only the tab content so I can use a VStack
, or something else.
This existing solution attempts to remove the background of the TabView does not seem to work. I agree with a commenter that says that workaround appears to have been broken in Later OS versions.
var body: some View {
ZStack {
randomColor
TabView {
ForEach(pages) { page in
Color.clear
// https://stackoverflow.com/questions/63178381/make-tabview-background-transparent
.background(BackgroundHelper())
.tag(page)
.tabItem {
Label(page.rawValue, systemImage: page.rawValue)
}
}
}
}
}
Your UIViewRepresentable
implementation is incorrect. You should always return a new instance of the view in makeUIView
. With the current implementation, you only ever have one instance of WKWebView
.
As you switch between tabs, SwiftUI adds that WKWebView
as subviews of the different tabs. Obviously, a UIView
can only have one parent view, so as you move to another tab, the WKWebView
gets removed from the previous tab. When you move back to a visited tab, the WKWebView
is not added back, because SwiftUI doesn't expect it to be removed in the first place.
Here is a more correct implementation:
struct RandomColorHostedWebView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ })
let webConfiguration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
let urlRequest = URLRequest(url: URL(string: "https://randomcolour.com")!)
webView.load(urlRequest)
return webView
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
The BackgroundHelper
from the linked post can be slightly modified to use a UIViewControllerRepresentable
- going up the UIViewController
hierarchy instead of the UIView
hierarchy. Of course, there is no guarantee that this is not going to break in the future.
struct BackgroundHelper: UIViewControllerRepresentable {
class HelperVC: UIViewController {
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
var currParent = parent
while true {
guard let parent = currParent else {
break
}
if parent.tabBarController?.viewControllers?.contains(parent) == true {
currParent?.view.backgroundColor = .clear
parent.tabBarController?.view.backgroundColor = .clear
break
}
currParent = currParent?.parent
}
}
}
func makeUIViewController(context: Context) -> HelperVC {
HelperVC()
}
func updateUIViewController(_ uiViewController: HelperVC, context: Context) {
}
}
Then you can write
ZStack {
RandomColorHostedWebView()
TabView {
ForEach(pages) { page in
Text("Foo")
.background { BackgroundHelper() }
.tag(page)
.tabItem {
Label(page.rawValue, systemImage: page.rawValue)
}
}
.toolbarBackgroundVisibility(.visible, for: .tabBar)
}
}
.ignoresSafeArea()
I used ignoresSafeArea
and toolbarBackgroundVisibility
to make it look nicer. Depending on what web content you are actually displaying, you might not want to ignore the bottom safe area (i.e. you want to place the web view just above the tab bar), in which case you can do:
@Namespace var ns
// ...
ZStack {
RandomColorHostedWebView()
.matchedGeometryEffect(id: "tab", in: ns, isSource: false)
TabView {
ForEach(pages) { page in
// we only want the source of the matched geometry effect to be on the initial tab only, hence this check
if page == .settings {
ZStack {
Color.clear
.matchedGeometryEffect(id: "tab", in: ns, isSource: true)
Text("Foo")
}
.ignoresSafeArea(edges: [.horizontal, .top])
.background { BackgroundHelper() }
.tag(page)
.tabItem {
Label(page.rawValue, systemImage: page.rawValue)
}
} else {
Text("Foo")
.background { BackgroundHelper() }
.tag(page)
.tabItem {
Label(page.rawValue, systemImage: page.rawValue)
}
}
}
.toolbarBackgroundVisibility(.visible, for: .tabBar)
}
}
.ignoresSafeArea(edges: [.horizontal, .top])