大家好,咱们又碰头了~今日给大家带来 Tabs标签页组件在SwiftUI中的完结办法。在本文中,我依然会选用一种循序渐进的办法来进行解说,这其实也是我的完结思路,期望能帮到需求的朋友。
在看本文之前,我强烈建议你先阅读我的上一篇文章 SwiftUI精讲:自定义 Tabbar 组件 (包括过渡作用),因为有一些重复的常识点在上篇中现已讲过了,本文再讲的话难免会有些庸俗,我期望每次写下的文章都有一些新的常识点~
1.Tabs组件的完结
咱们先创立 Componets 文件夹,并在其中创立 tabs 文件,咱们先简单地创立一个list,并将内容遍历烘托出来,如下所示:
1-1:大致UI的完结
import SwiftUI
struct TabItem: Identifiable {
var id:Int
var text:String
}
struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 0
var body: some View {
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
currentSelect = tabItem.id
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(.horizontal,12)
.fixedSize()
Spacer()
}
}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
}
}
struct tabs_Previews: PreviewProvider {
// 创立一些测试数据
static let list = [
TabItem(id:1,text:"重视"),
TabItem(id:2,text:"引荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端")
]
static var previews: some View {
tabs(list: list)
}
}
这儿加上 .frame(minWidth: UIScreen.main.bounds.width)
是为了保证在标签只有两三个的时分,我依然期望它们处于一个均匀布局的状态。代码运转后如图所示:

接着咱们加上下划线款式,代码如下所示:
import SwiftUI
struct TabItem: Identifiable {
var id:Int
var text:String
}
struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 1
var body: some View {
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
currentSelect = tabItem.id
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
Spacer()
}
.background(
VStack{
if(currentSelect == tabItem.id){
Spacer()
Rectangle()
.fill(Color(hex: "#1677ff"))
.frame(height: 2)
.padding(.horizontal,12)
.cornerRadius(2)
}
}
)
}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
}
}
struct tabs_Previews: PreviewProvider {
// 创立一些测试数据
static let list = [
TabItem(id:1,text:"重视"),
TabItem(id:2,text:"引荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端")
]
static var previews: some View {
tabs(list: list)
}
}
细心的朋友可能会发现,我的代码里边出现了 Color(hex: "#1677ff")
,这是因为咱们对Color结构进行了拓宽,让它支持16进制色彩的传递,如下所示:
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
代码运转后的作用如图所示:

咱们再对字体方面进行优化,咱们期望点击后的字体色彩和巨细和点击前保持不一致,咱们对代码做出修正,如下所示:
HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
// 新增
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
// 新增
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
Spacer()
}
更改后的作用如图所示:

好了,咱们一个一般的tab组件就写完了,结束撒花。
接下来咱们需求给下划线添加相应的过渡作用,相似于的下划线移动过渡。假如有从事web端开发的朋友们,咱们能够想一下,在web端咱们是怎么完结相似的作用的?是不是要经过一些计算,然后赋值给下划线 css的 left 值,或者是 translateX 值。在SwiftUI中,咱们压根不用这么费事,咱们能够运用 matchedGeometryEffect 来轻易的做到相应的作用!
1-2:下划线过渡作用完结
咱们对代码稍微修正下,具体的过程我会在图中进行标注,如下图所示:

接着咱们按下 command + R
,运转 Simulator 来查看对应的作用:

能够发现,咱们其完结已取得了咱们想要的作用。可是因为 tab 在激活的时分,文字对应的动画看着非常晃眼,很讨人厌。假如期望只保存下划线的过渡作用,而不要文字的过渡作用,该怎么做呢?
很简单,咱们只需求添加 .animation(nil,value:UUID())
即可,如下所示:
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
// 新增
.animation(nil,value:UUID())
现在看起来是不是正常多了?

1-3:主动滚动到对应方位
大致UI画得差不多了,接下来咱们需求在点击比较靠后的tab时,咱们期望 ScrollView 能帮咱们滚动到对应的方位,咱们该怎么做呢? 答案是引进 ScrollViewReader, 运用 ScrollViewProxy中的scrollTo办法,代码如下所示:
struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 1
@Namespace var animationNamespace
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
withAnimation{
currentSelect = tabItem.id
}
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
.animation(nil,value:UUID())
Spacer()
}
.background(
VStack{
if(currentSelect == tabItem.id){
Spacer()
Rectangle()
.fill(Color(hex: "#1677ff"))
.frame(height: 2)
.padding(.horizontal,12)
.cornerRadius(2)
.matchedGeometryEffect(id: "tab_line", in: animationNamespace)
}
}
)
}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
.onChange(of: currentSelect) { newSelect in
withAnimation(.easeInOut) {
scrollProxy.scrollTo(currentSelect,anchor: .center)
}
}
}
}
}
在代码中,咱们利用 scrollProxy.scrollTo 办法,轻易地完结了滚动到对应tab的方位。作用如下所示:

呜呼,目前为止,咱们现已完结了一个不错的tabs组件。接下来在ContentView中,咱们引进该组件。因为咱们需求在父视图中知道tabs中currentSelect的改变,咱们需求把子组件的 @State 改成 @Binding,同时为了避免 preview报错,咱们也要做出对应的修正,如图所示:

1-4:结合TabView完结手势滑动切换
日常咱们在运用tabs标签页的时分,假如需求支持用户经过手势进行切换标签页的操作,咱们能够结合TabView一起运用,代码如下所示:
import SwiftUI
struct ContentView: View {
let list = [
TabItem(id:1,text:"重视"),
TabItem(id:2,text:"引荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端"),
]
@State var currentSelect:Int = 1
var body: some View {
VStack(spacing: 0){
tabs(list:list,currentSelect:$currentSelect)
TabView(selection:$currentSelect){
ForEach(list){tabItem in
Text(tabItem.text).tag(tabItem.id)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
作用如下所示:

至此,咱们总算是完结了一个能满足大部分需求的Tabs组件啦~
2. Tabs组件的拓宽
2-1:Tabs组件的吸顶
仅仅完结一个简单的作用怎么够,这不符合笔者精讲技术的精神,咱们还要结合日常的业务进行考虑。比如,我现在想要在页面滚动的时分,我期望tabs组件能够主动吸顶,应该怎么去完结呢?
首要咱们新建View文件夹,在其中放置一些视图组件,并在组件中,添加一些文本,如图所示:

LazyVStack + Section
的办法来做。可是有个问题,TabView被包裹在Section里边时,TabView的高度会丢失。我将会在 ScrollView 的外层套上 GeometryReader 来解决这个问题,以下为代码展现:
import SwiftUI
struct ContentView: View {
let list = [
TabItem(id:1,text:"重视"),
TabItem(id:2,text:"引荐"),
TabItem(id:3,text:"热榜")
]
@State var currentSelect:Int = 1
var body: some View {
NavigationView{
GeometryReader { proxy in
ScrollView{
LazyVStack(spacing: 0, pinnedViews:.sectionHeaders) {
Section(
header:tabs(list:list,currentSelect:$currentSelect)
.background(.white)
){
TabView(selection:$currentSelect){
ForEach(list){tabItem in
VStack{
switch currentSelect{
case 1:
Attention()
case 2:
Recommend()
case 3:
Hot()
default:
Text("")
}
}
.tag(tabItem.id)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(minHeight:proxy.size.height)
}
}
}
}
.navigationTitle("Tabs组件完结")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
作用如图所示:

2-2:下拉刷新的完结
要完结下拉刷新的功用,咱们能够运用ScrollView并结合.refreshable
来完结这个作用,代码如下所示:
import SwiftUI
struct ContentView: View {
let list = [
TabItem(id:1,text:"重视"),
TabItem(id:2,text:"引荐"),
TabItem(id:3,text:"热榜")
]
@State var currentSelect:Int = 1
var body: some View {
NavigationView{
GeometryReader { proxy in
ScrollView{
LazyVStack(spacing: 0, pinnedViews:.sectionHeaders) {
Section(
header:tabs(list:list,currentSelect:$currentSelect)
.background(.white)
){
TabView(selection:$currentSelect){
ForEach(list){tabItem in
ScrollView{
switch currentSelect{
case 1:
Attention()
case 2:
Recommend()
case 3:
Hot()
default:
Text("")
}
}
.tag(tabItem.id)
.refreshable {
print("触发刷新")
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(minHeight:proxy.size.height)
}
}
}
}
.navigationTitle("Tabs组件完结")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
在这儿要注意 .refreshable
是 ios15 才能运用的,运用时要考虑API的兼容性。作用如图所示:

至此,咱们现已完结了一个很不错的Tabs标签页组件啦。感谢你的阅读,如有问题欢迎在评论区中进行沟通~