Closed Goktug closed 4 months ago
+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 presentable
s.
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.
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 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 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 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
}))
}
}
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?
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...
@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.
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 viafocusFirst
and our custom tab bar changes accordingly (since we are getting the index value fromchild.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 )
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 viafocusFirst
and our custom tab bar changes accordingly (since we are getting the index value fromchild.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 )
Did you manage to solve this problem later on? If so, how did you do it?
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 :-)
@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!
@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
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)
}
}
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