在SwiftUI中实现自定义的高度灵活的标签栏
标签栏组件是UI界面中非常重要的组件,它通过创建一组清晰的选项卡按钮(标签),使得用户能够轻松地识别并切换到不同的内容区域,从而在一个界面上展现多个不同主题的丰富内容 。
SwiftUI提供了一个内置的标签栏组件,即TabView组件,但这个组件不够灵活,虽然通过定制可以解决一些问题,但很难适应一些更复杂的场景。
这篇博文介绍一种完全自定义的高度灵活的标签栏实现方式,可以作为SwiftUI 的标签栏组件的补充。
设计思路
通常标签栏组件可以分解为三部分: 标签部分(Tab),标签栏部分(TabBar),以及标签内容部分,即TabPage。我们的思路是,将这三部分独立设计,让它们独立发展,从而可以提供各种组合以实现高度灵活性。 标签部分需要数据支持,可以是数组,也可以是枚举,或者其他类型的数据, 我们用协议实现;标签栏的主要功能是对一组标签进行布局排列,我们用定制的组件实现;标签内容部分主要显示当前标签的内容,这个和具体业务需求相关,无需任何设计,直接在业务代码中实现,但需要遵循一定规范。 如下是我们的设计代码:
//标签协议
protocol HDXTab
{
associatedtype TAB:View
@ViewBuilder
func makeTab(isSelected:Bool,action:@escaping ()->Void)->TAB
}
//标签栏组件
struct HDXTabBar<Content:View>:View
{
var content: ()->Content
var body:some View
{
self.content()
}
}
上面代码中,大家可能会觉得标签栏组件很奇怪,似乎和标签(Tab)没有任何关联,诀窍在于: 在标签栏的Content闭包中,我们可以直接引用标签的数据: 数组或者枚举。换句话说,这个关联是在闭包中完成的。
其实整个标签栏组件也是可以完全省略的,因为它的工作主要是对各标签进行排列布局,用SwiftUI提供各种内置布局组件就可以实现。之所以要设计标签栏组件,仅仅是需要通过一个组件名称以强化语义。
代码示例
下面按上述思路实现一个简单的普通的标签栏。效果如下图:。
我们用枚举为该组件建模。
枚举和标签栏组件有一种天然的联系,建议优先考虑用枚举为标签栏组件建模
//用枚举建模
enum DemoTab:CaseIterable
{
case 首页
case 地址
case 收藏
case 我的
func getText()->String
{
return "\(self)"
}
}
//对DemoTab进行扩展,使其遵循HDXTab协议:
extension DemoTab:HDXTab
{
@ViewBuilder
func makeTab(isSelected : Bool, action: @escaping () -> Void) ->some View
{
Button
{
action()
} label:
{
Text( self.getText())
.foregroundColor(isSelected ? .blue : .black)
}
}
}
下面是使用DemoTab组件的代码:
struct TabViewDemo:View
{
@State var selectedTab = DemoTab.首页
var body :some View
{
VStack
{
ZStack
{
//TabPage
// Text( selectedTab.getText())
switch selectedTab {
case .首页: homeView()
case .地址: addressView()
case .收藏: favoriteView()
case .我的: profileView()
}
}.frame(width:200,height:100)
//TabBar
HDXTabBar
{
HStack {
Spacer()
ForEach (DemoTab.allCases,id:\.self) {
item in
//Tab
item.makeTab(isSelected:selectedTab == item)
{
selectedTab = item
}
Spacer()
}
}
}
}
}
@ViewBuilder
func homeView()->some View
{
Text("this is home view")
}
@ViewBuilder
func addressView()->some View
{
Text("this is address view")
}
@ViewBuilder
func favoriteView()->some View
{
Text("this is favorite view")
}
@ViewBuilder
func profileView()->some View
{
Text("this is profile view")
}
}
上述代码中,我们在标签栏组件中直接引用DemoTab枚举的各个枚举项,并调用其makeTab()方法构造标签,然后对标签进行布局。
另外,请特别注意标签切换的代码,以及展示标签内容(标签页)的代码,看起来似乎有些多,但由于使用了枚举建模,显得清晰和有条理。 HDXTabBar 这一层可以完全拿掉,如前所叙,仅仅是为了强调语义。
可以尝试对上面标签栏进行一些定制。比如,给每个标签加上图标,动画等等,将标签内容放置在标签栏之上,或者左面,或者右面,亦或者当标签比较多时,使用两行布局标签标签等等,这些需求在我们的实现中都非常容易实现。
小结
本文介绍了一种实现标签栏的方式,主要特点是让标签(Tab),标签栏(TabBar)以及标签内容(TabPage)独立变化,灵活组合,从而具有高度的灵活性。灵活性包括但不限于下面几个方面:
- 定制的Tab风格
- 定制的TabBar风格
- 定制TabBar和TabPage之间的位置关系