Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using a Custom TabBar #42

Closed
Goktug opened this issue Nov 20, 2021 · 14 comments
Closed

Using a Custom TabBar #42

Goktug opened this issue Nov 20, 2021 · 14 comments

Comments

@Goktug
Copy link

Goktug commented Nov 20, 2021

In my project, I need to calculate the tab height. However, there is no way to access the existing TabBar in your library. If you can add the height value as an environment variable, would be nice. On the other hand, I am thinking that if you can add an ability to use Custom Tab Bar would be awesome.

What do you think? I'd like to hear your opinions

@Goktug Goktug changed the title Using Custom a TabBar Using a Custom TabBar Nov 20, 2021
@YuantongL
Copy link

+1

@Goktug I personally have a hack to achieve this, Overriding func view() -> AnyView function in TabCoordinatable gives you opportunity to use your own custom tab bar, you can create one using UIKit or use a pure SwiftUI implementation as you like.

To make it all working, this custom tab bar should mimic the TabCoordinatableView, basically follow its init, this is where the hack comes in, I have to use a @testable import Stinsen to access things like coordinator.child.allItems, and its presentables.

Expose these internal variables to public will do it, but a nicer way is - we can pass in our own ViewBuilder for tab bar and their associated views.

@Goktug
Copy link
Author

Goktug commented Nov 20, 2021

Thanks for the advice @YuantongL, I was also trying to hack but couldn't reach the active tab index which is reactive. At least, if the library could provide this value we don't need to use @testable hack

@YuantongL
Copy link

YuantongL commented Nov 20, 2021

@Goktug I think #43 this is all we need in order to create a customized tab bar and tab view.

@Goktug
Copy link
Author

Goktug commented Nov 21, 2021

@YuantongL I found a way without too much hacking.

First, you need to hide the UITabBar globally.

UITabBar.appearance().isHidden = true

And then you need to create an environment variable to pass active tab value to inside through subviews

struct ActiveTab: EnvironmentKey {
  static let defaultValue: Int = 0
}

we need to use the customize method of the coordinator to be able to place our custom tab bar and then we need to pass activeTab value to be able to make our custom tab bar fully functional

  @ViewBuilder func customize(_ view: AnyView) -> some View {
    ZStack(alignment: .bottom) {
      view
      CustomTabBarView() // <-- This is our custom tab bar
    }
    .environment(\.activeTab, self.child.activeTab)
  }

Every time user changed the tab, we need to pass the new active tab index value. Therefore, we'll be using tab item creating @ViewBuilder methods

  @ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
      EmptyView()
        .environment(\.activeTab, self.child.activeTab)
  }

Almost everything is done, the only thing that we need to do is, handling the navigation when user click a specific custom tab button

  Button {
    _ = router <-- Injecting coordinator router
      .focusFirst(\.home)
      .child
  } label: {
     // activeTab == 0 <-- You can change the UI with checking the active tab index value
     // Tab Item UI omitted
  }

Finally, I managed to create a custom tab bar using this structure without hacking the library. I hope I would be helpful to you as well

@YuantongL
Copy link

@Goktug Thanks, that's a nice approach, definitely better then the hack!

I made similar change to my project, the only difference is, instead of pass in environment variable, I made it through a binding.

struct CustomTabBarView: View {
    @Binding
    var activeTabIndex: Int
    var body: some View {
        HStack {
            Button {
                activeTabIndex = 0
            } label: {
                Text("Tab 0")
            }
            Button {
                activeTabIndex = 1
            } label: {
                Text("Tab 1")
            }
    }
}

Then in the coordinator, do the following

    private var activeTabIndex = 0 {
        didSet {
            child.activeTab = activeTabIndex
        }
    }

    @ViewBuilder func customize(_ view: AnyView) -> some View {
        ZStack(alignment: .bottom) {
            view
            CustomTabBarView(activeTabIndex: .init(get: {
                self.activeTabIndex
            }, set: { newValue in
                self.activeTabIndex = newValue
            }))
        }
    }

@Goktug
Copy link
Author

Goktug commented Nov 21, 2021

Your approach only relies on tab clicks, if you want to navigate through tabs via the router, your approach will fail. E.g. deep link. WDYT?

@rundfunk47
Copy link
Owner

Hi! Instead of a TabCoordinatable, a NavigationCoordinatable can also be used with your previous workaround @Goktug. Then you don't need to hide the tabbar globally - and instead of switching the tab you can use the setRoot-function. I could whip up an example if things are still unclear later...

@YuantongL
Copy link

@Goktug my approch doesn't have to depend on tab clicks if I simply remove the private activeTabIndex and use

    @ViewBuilder func customize(_ view: AnyView) -> some View {
        ZStack(alignment: .bottom) {
            view
            CustomTabBarView(activeTabIndex: .init(get: {
                self.child.activeTab
            }, set: { newValue in
                self.child.activeTab = newValue
            }))
        }
    }

Anything that has a TabCoordinator.Router can change tab via focusFirst and our custom tab bar changes accordingly (since we are getting the index value from child.activeTab).
I think both approach works, it is just personally I prefer pass around a Binding in this case instead of environment object.

@2jumper3
Copy link

2jumper3 commented Jan 11, 2023

How

@Goktug my approch doesn't have to depend on tab clicks if I simply remove the private activeTabIndex and use

    @ViewBuilder func customize(_ view: AnyView) -> some View {
        ZStack(alignment: .bottom) {
            view
            CustomTabBarView(activeTabIndex: .init(get: {
                self.child.activeTab
            }, set: { newValue in
                self.child.activeTab = newValue
            }))
        }
    }

Anything that has a TabCoordinator.Router can change tab via focusFirst and our custom tab bar changes accordingly (since we are getting the index value from child.activeTab). I think both approach works, it is just personally I prefer pass around a Binding in this case instead of environment object.

Hi! How you resolve problem if u need to hide tabView? Custom tab view always showing if u use this method )
photo_2023-01-11 11 21 40

@alvin-7
Copy link

alvin-7 commented Feb 3, 2024

How

@Goktug my approch doesn't have to depend on tab clicks if I simply remove the private activeTabIndex and use

    @ViewBuilder func customize(_ view: AnyView) -> some View {
        ZStack(alignment: .bottom) {
            view
            CustomTabBarView(activeTabIndex: .init(get: {
                self.child.activeTab
            }, set: { newValue in
                self.child.activeTab = newValue
            }))
        }
    }

Anything that has a TabCoordinator.Router can change tab via focusFirst and our custom tab bar changes accordingly (since we are getting the index value from child.activeTab). I think both approach works, it is just personally I prefer pass around a Binding in this case instead of environment object.

Hi! How you resolve problem if u need to hide tabView? Custom tab view always showing if u use this method ) photo_2023-01-11 11 21 40

Did you manage to solve this problem later on? If so, how did you do it?

@2jumper3
Copy link

Did you manage to solve this problem later on? If so, how did you do it?

Hi @alvin-7 ! Yes, remove everything and create TabBar with UIKit :-)

@jedmund
Copy link

jedmund commented May 17, 2024

@2jumper3 Can you go into a bit more detail on how you removed the existing tab bar? I have a tab bar implementation I want to use that I'm trying to figure out the best way to implement. Thanks!

@pulimento
Copy link

@2jumper3 Can you go into a bit more detail on how you removed the existing tab bar? I have a tab bar implementation I want to use that I'm trying to figure out the best way to implement. Thanks!

this comment worked for me to implement a view that replaces the tabview with something custom

@est7
Copy link

est7 commented Jul 5, 2024

first of all,set hide UITabBar in your app:

UITabBar.appearance().isHidden = true

then:


enum Tab: Int {
  case home = 0
  case chat, profile
}



final class AppCoordinator: TabCoordinatable {
  var child: Stinsen.TabChild = TabChild(
    startingItems: [
      \AppCoordinator.homePage,
      \AppCoordinator.chatPage,
      \AppCoordinator.profilePage,
    ])

  private var activeTab: Tab = .home {
    didSet {
      self.child.activeTab = activeTab.rawValue
    }
  }

  @ViewBuilder
  func customize(_ view: AnyView) -> some View {
    ZStack(alignment: .bottom) {
      view
      BottomNavigationBar(
        selectedTab: .init(
          get: {
            return Tab(rawValue: self.child.activeTab) ?? .home
          },
          set: { newValue in
            self.activeTab = newValue
          }
        )
      )
    }
  }

  @Route(tabItem: makeHomeTabItem)
  var homePage = makeHomeCoordinator

  @Route(tabItem: makeChatTabItem)
  var chatPage = makeChatCoordinator

  @Route(tabItem: makeProfileTabItem)
  var profilePage = makeProfileCoordinator

  @ViewBuilder
  func makeHomeTabItem(isActive: Bool) -> some View {
    // Actually it won't be shown here
    Label("Home", systemImage: "house")
  }

  @ViewBuilder
  func makeChatTabItem(isActive: Bool) -> some View {
    Label("Chat", systemImage: "message")
  }

  @ViewBuilder
  func makeProfileTabItem(isActive: Bool) -> some View {
    Label("Profile", systemImage: "person")
  }
}

my BottomNavigationBar:

struct BottomNavigationBar: View {
   @Binding var selectedTab: Tab
   
   var body: some View {
       HStack {
           BottomNavigationTabItem(icon: "house", title: "Home", tab: .home, selectedTab: $selectedTab)
           Spacer()
           BottomNavigationTabItem(icon: "message", title: "Chat", tab: .chat, selectedTab: $selectedTab)
           Spacer()
          BottomNavigationTabItem(icon: "person", title: "Me", tab: .profile, selectedTab: $selectedTab)
       }
       .padding()
       .background(
           BlurView(style: .systemMaterial)
               .clipShape(RoundedRectangle(cornerRadius: 35.0, style: .continuous))
       )
       .padding(.horizontal,20)
       .padding(.bottom, 10)
   }
}

@Goktug Goktug closed this as completed Jul 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants