stinsen icon indicating copy to clipboard operation
stinsen copied to clipboard

Using a Custom TabBar

Open Goktug opened this issue 2 years ago • 12 comments

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 avatar Nov 20 '21 13:11 Goktug

+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.

YuantongL avatar Nov 20 '21 16:11 YuantongL

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

Goktug avatar Nov 20 '21 18:11 Goktug

@Goktug I think https://github.com/rundfunk47/stinsen/pull/43 this is all we need in order to create a customized tab bar and tab view.

YuantongL avatar Nov 20 '21 19:11 YuantongL

@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

Goktug avatar Nov 21 '21 09:11 Goktug

@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
            }))
        }
    }

YuantongL avatar Nov 21 '21 18:11 YuantongL

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?

Goktug avatar Nov 21 '21 22:11 Goktug

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...

rundfunk47 avatar Nov 22 '21 15:11 rundfunk47

@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.

YuantongL avatar Dec 02 '21 02:12 YuantongL

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

2jumper3 avatar Jan 11 '23 07:01 2jumper3