I am working on a PWA wrapper, in which I want to move certain parts of the PWA to the native APP. For example: login/authentication (fingerprint/Face ID) logic, navigation logic (tabbar), notifications.
For iOS 18/26 I have found that a TabView with Tab items would be a great base to start with. Together with a WKWebView (iOS 18) and WebView (iOS 26) it looked like a match made in heaven.
I first started for iOS 26, when that's working I will create the fallbacks for iOS 18.
But I quickly ran into trouble with the new WebView/WebPage components.
My first setup was:
struct ContentView: View {
@State private var selectedTab: Tabs = .home
var body: some View {
TabView(selection: $selectedTab) {
ForEach(Tabs.allCases, id: \.self) { tab in
Tab(tab.title, systemImage: tab.icon, value: tab) {
WebView(url: tab.url)
}
}
}
}
}
That works, but not the way I wanted. The above code creates a new WebView instance for each Tab, that causes all sorts of issues (increased memory usage, trouble with cookies/sessions) and it's just plain ugly.
I then tried some versions with a WebView wrapper in which I tried the WebView and WebPage as static variables. The idea was good, but I quickly ran into exceptions when I tried to navigate to a different URL.
With some help of a few AI services I discovered this had to do with a WebPage that should not be re-used in multiple WebView instances (even though it was static), and probably some other issues caused by my lack of Swift(UI). I stopped working on iOS apps when Objective-C still was the main language.
Eventually (again with the help of AI) I managed to get a somewhat working situation:
struct ContentView: View {
@State private var selectedTab: Tabs = .home
var body: some View {
BrowserContainer()
TabView(selection: $selectedTab) {
ForEach(Tabs.allCases, id: \.self) { tab in
Tab(tab.title, systemImage: tab.icon, value: tab) {
Color.red // I made this red to see why the WebView only was 50% of the display height
.onAppear() {
WebViewManager.shared.load(tab.url)
}
}
}
}
}
}
@MainActor
final class WebViewManager: ObservableObject {
static let shared = WebViewManager()
let page: WebPage
let webView: WebView
private init() {
self.page = WebPage()
self.webView = WebView(page)
}
func load(_ url: URL) {
page.load(URLRequest(url: url))
}
}
struct BrowserContainer: View {
@ObservedObject private var manager = WebViewManager.shared
var body: some View {
manager.webView
.ignoresSafeArea()
}
}
But being a C# developer for a living, the resulting code made me sick. It was ugly and hacky, and I was sure that it's not the way to go. So that's what made me decide to create this post and hopefully get some knowledge of experienced developers that made something similar to this.
The ultimate goal is one global WebView (and eventually WKWebView for iOS 18) instance, which I can then "navigate" with code by changing the URL. This would be main navigation of different pages using the TabBar, but also special URLs from Notifications.
As of today, I am unaware of any SwiftUI-native solution for maintaining a single View while using the TabBar logic to shuffle between Data.
# One possible solution using UIKit...
I created a protocol, TabBarOption. This protocol was created with enum's in-mind, and is used to tell CustomTabBar what's coming and what's available. Usage is simple:
enum TabOption: Int , TabBarOption {
case home
var title: String {…}
var image: String {…}
}
struct MyTabView: View {
@State var selection: TabOption
var body: some View {
MyDataView ( selection: selection )
CustomTabBar ( selection: $selection )
}
}
‘CustomTabBar’ only produces a TabBar, not the associated View.
$selection will update the local @State case of the enum.
Only The webKit logic is available in iOS 26 and later. The TabBar should cover as far back as iOS 14? Maybe earlier.
It's worth noting that this approach looses the "more" feature, where TabViews with more that 4 tabs are automatically placed inside of a "more" tab.
Tested and working on iPhone 15 , iOS 26.
import SwiftUI
import WebKit
import UIKit
@available( iOS 26.0 , * )
struct DemoView: View {
@State var manager: WebManager = WebManager ( .fox )
var body: some View {
VStack {
WebView ( manager.page )
CustomTabBar ( selection: $manager.webSite )
}
.task ( id: manager.webSite ) { manager.load ( manager.webSite ) }
.ignoresSafeArea ( .all , edges: .bottom )
.padding ( .top , 1 )
}
}
@available( iOS 26.0 , * )
@Observable
class WebManager {
var webSite: WebSite
var page: WebPage = .init()
init ( _ webSite: WebSite ) {
self.webSite = webSite
self.page.load ( webSite.url )
}
func load ( _ newWebsite: WebSite ) {
self.webSite = newWebsite
self.page.load ( newWebsite.url )
}
}
enum WebSite: Int , TabBarOption {
case home , cnn , fox , msnbc
var url: URL? {
switch self {
case .home : URL ( string: "https://www.stackoverflow.com" )
case .cnn : URL ( string: "https://www.cnn.com" )
case .fox : URL ( string: "https://www.foxnews.com" )
case .msnbc : URL ( string: "https://www.msnbc.com" )
}
}
var title: String {
switch self {
case .home : "Stack Overflow"
case .cnn : "CNN"
case .fox : "Fox"
case .msnbc : "MSNBC"
}
}
var image: String {
switch self {
case .home : "paperclip"
case .cnn : "backpack"
case .fox : "pencil"
case .msnbc : "newspaper"
}
}
}
public protocol TabBarOption: CaseIterable , Hashable , RawRepresentable where Self.RawValue == Int {
static var home: Self { get }
var title: String { get }
var image: String { get }
}
public struct CustomTabBar < Case: TabBarOption >: UIViewRepresentable {
@Binding var selection: Case
let items: [ UITabBarItem ]
public init ( selection: Binding < Case > ) {
self._selection = selection
self.items = Case.allCases.map { UITabBarItem ( title: $0.title , image: UIImage ( systemName: $0.image ) , tag: $0.rawValue ) }
}
public func makeUIView ( context: Context ) -> UITabBar {
let tabBar = UITabBar()
tabBar.items = items
tabBar.selectedItem = items [ selection.rawValue ]
tabBar.delegate = context.coordinator
return tabBar
}
public func updateUIView ( _ uiView: UITabBar , context: Context ) { uiView.selectedItem = items [ selection.rawValue ] }
public func makeCoordinator() -> Coordinator { Coordinator ( self ) }
public class Coordinator: NSObject, UITabBarDelegate {
var parent: CustomTabBar
init ( _ parent: CustomTabBar ) { self.parent = parent }
public func tabBar ( _ tabBar: UITabBar , didSelect item: UITabBarItem ) {
parent.selection = Case ( rawValue: item.tag ) ?? .home
}
}
}