Home > other >  SwiftUI custom TabView with paging style
SwiftUI custom TabView with paging style

Time:02-03

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:

  1. You create a struct that holds the View & the tag;
  2. tabBarItem should now return the previously created struct;
  3. 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)
    }
}
  • Related