I'm trying to create a custom TabView
in SwiftUI, that also has a .tabViewStyle(.page())
functionality too.
At the moment I'm 99% of the way there, but cannot figure out how to get all the TabBarItems
to list.
I'm using the PreferenceKey so that the order I add them into the closure is the order in the TabView.
When I run it, the tab items are added to the array, then removed, and it doesn't seem to be working.
I had it working with the enum as CaseIterable
and the ForEach(tabs) { tab in
as ForEach(TabBarItems.allCases) { tab in
, but as mentioned wanted the order in the bar to be organic from the clousure.
Container
struct TabViewContainer<Content : View>: View {
@Binding private var selection: TabBarItem
@State private var tabs: [TabBarItem] = []
var content: Content
init(selection: Binding<TabBarItem>, @ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
content
}
.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
.onPreferenceChange(TabBarItemsPreferenceKey.self) { self.tabs = $0 }
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}
PreferenceKey / Modifier
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value = nextValue()
}
}
struct TabBarItemViewModifier: ViewModifier {
let tab: TabBarItem
func body(content: Content) -> some View {
content.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(_ tab: TabBarItem) -> some View {
modifier(TabBarItemViewModifier(tab: tab))
}
}
Demo view
struct TabSelectionView: View {
@State private var selection: TabBarItem = .itinerary
var body: some View {
TabViewContainer(selection: $selection) {
PhraseView()
.tabBarItem(.phrases)
ItineraryView()
.tabBarItem(.itinerary)
BudgetView()
.tabBarItem(.budget)
BookingView()
.tabBarItem(.bookings)
PackingListView()
.tabBarItem(.packing)
}
}
}
Intended | Current |
---|---|
CodePudding user response:
You can use a more elegant way, @resultBuilder
:
- You create a struct that holds the
View
& the tag; tabBarItem
should now return the previously created struct;- The
@resultBuilder
will then build your array of your view & tag which you'll be using inside the container.
ResultBuilder:
@resultBuilder
public struct TabsBuilder {
internal static func buildBlock(_ components: Tab...) -> [Tab] {
return components
}
internal static func buildEither(first component: Tab) -> Tab {
return component
}
internal static func buildEither(second component: Tab) -> Tab {
return component
}
}
Tab:
struct Tab: Identifiable {
var content: AnyView //I don't recommend the use of AnyView, but I don't want to dive deep into generics for now.
var tag: TabBarItem
var id = UUID()
}
Modifier:
struct Tab: Identifiable {
var content: AnyView
var tag: TabBarItem
var id = UUID()
}
TabViewContainer:
struct TabViewContainer: View {
@Binding private var selection: TabBarItem
@State private var tabs: [TabBarItem]
var content: [Tab]
init(selection: Binding<TabBarItem>, @TabsBuilder content: () -> [Tab]) {
self._selection = selection
self.content = content()
self.tabs = self.content.map({$0.tag})
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
ForEach(content) { content in
content.content
.tag(content.tag)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}