import Foundation import SwiftUI struct SimpleTab: View, Identifiable { var id: SelectionValue var image: Image var label: Text var content: AnyView init(id: SelectionValue, @ViewBuilder icon: (SelectionValue) -> TupleView<(Image, Text)>, @ViewBuilder content: (SelectionValue) -> Content) { let tabIcon = icon(id) self.id = id self.image = tabIcon.value.0 self.label = tabIcon.value.1 self.content = AnyView(content(id)) } var body: some View { return self.content } } struct SimpleTabView: View { /* SwiftUI's TabView is riddled with lifecycle bugs: 1. It creates view trees for tabs which is not visible 2. It yields an extra onAppear, onDisappear and onAppear when making a tab visible 3. It yields an extra onApepar callback when the whole tab is removed */ @Binding var selection: SelectionValue var tabs: [SimpleTab] @State var shown: [SelectionValue] = [] @ViewBuilder var body: some View { GeometryReader { proxy in ZStack(alignment: .center) { VStack(alignment: .center, spacing: 0) { HStack(alignment: .center, spacing: 0) { Spacer(minLength: 0) VStack(alignment: .center, spacing: 0) { Spacer(minLength: 0) ZStack(alignment: .center) { ForEach(tabs) { tab in if shown.contains(tab.id) || selection == tab.id { tab.content .hide(selection != tab.id).id(tab.id) .onAppear { if !self.shown.contains(tab.id) { self.shown.append(tab.id) } } } } } Spacer(minLength: 0) } Spacer(minLength: 0) } VStack(alignment: .center, spacing: 0) { Rectangle() .frame(height: 0.5) .foregroundColor(Color.black.opacity(0.2)) HStack(alignment: .center, spacing: 0) { ForEach(tabs) { tab in Button(action: { self.selection = tab.id }, label: { VStack(alignment: .center, spacing: 0) { tab.image .resizable() .aspectRatio(contentMode: .fit) tab.label .padding(.top, 4) } .frame(width: itemWidth(for: proxy.size.width)) .frame(height: 44) .padding(.vertical, 8) }) .foregroundColor(foregroundColorForTabId(tab.id)) } } .padding(.top, 8) .frame(height: 64) .padding(.bottom, proxy.safeAreaInsets.bottom) } .background(Blur(style: .systemMaterial)) } .edgesIgnoringSafeArea(.bottom) } } } func itemWidth(for viewWidth: CGFloat) -> CGFloat { return (viewWidth / CGFloat(tabs.count)) } func foregroundColorForTabId(_ id: SelectionValue) -> Color { return id == selection ? .black : Color.black.opacity(0.3) } struct Blur: UIViewRepresentable { var style: UIBlurEffect.Style = .systemMaterial func makeUIView(context: Context) -> UIVisualEffectView { return UIVisualEffectView(effect: UIBlurEffect(style: style)) } func updateUIView(_ uiView: UIVisualEffectView, context: Context) { uiView.effect = UIBlurEffect(style: style) } } }