大家好,咱们又碰头了~今日给大家带来 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) 是为了保证在标签只有两三个的时分,我依然期望它们处于一个均匀布局的状态。代码运转后如图所示:

SwiftUI精讲:Tabs 标签页组件的实现

接着咱们加上下划线款式,代码如下所示:

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

代码运转后的作用如图所示:

SwiftUI精讲:Tabs 标签页组件的实现

咱们再对字体方面进行优化,咱们期望点击后的字体色彩和巨细和点击前保持不一致,咱们对代码做出修正,如下所示:

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

更改后的作用如图所示:

SwiftUI精讲:Tabs 标签页组件的实现

好了,咱们一个一般的tab组件就写完了,结束撒花。

接下来咱们需求给下划线添加相应的过渡作用,相似于的下划线移动过渡。假如有从事web端开发的朋友们,咱们能够想一下,在web端咱们是怎么完结相似的作用的?是不是要经过一些计算,然后赋值给下划线 css的 left 值,或者是 translateX 值。在SwiftUI中,咱们压根不用这么费事,咱们能够运用 matchedGeometryEffect 来轻易的做到相应的作用!

1-2:下划线过渡作用完结

咱们对代码稍微修正下,具体的过程我会在图中进行标注,如下图所示:

SwiftUI精讲:Tabs 标签页组件的实现

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

SwiftUI精讲:Tabs 标签页组件的实现

能够发现,咱们其完结已取得了咱们想要的作用。可是因为 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())

现在看起来是不是正常多了?

SwiftUI精讲:Tabs 标签页组件的实现

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的方位。作用如下所示:

SwiftUI精讲:Tabs 标签页组件的实现

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

SwiftUI精讲:Tabs 标签页组件的实现

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

作用如下所示:

SwiftUI精讲:Tabs 标签页组件的实现

至此,咱们总算是完结了一个能满足大部分需求的Tabs组件啦~

2. Tabs组件的拓宽

2-1:Tabs组件的吸顶

仅仅完结一个简单的作用怎么够,这不符合笔者精讲技术的精神,咱们还要结合日常的业务进行考虑。比如,我现在想要在页面滚动的时分,我期望tabs组件能够主动吸顶,应该怎么去完结呢?

首要咱们新建View文件夹,在其中放置一些视图组件,并在组件中,添加一些文本,如图所示:

SwiftUI精讲:Tabs 标签页组件的实现
接着咱们先考虑一下,如何在SwiftUI中做出一个吸顶的作用。这儿我运用了 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()
    }
}

作用如图所示:

SwiftUI精讲:Tabs 标签页组件的实现

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的兼容性。作用如图所示:

SwiftUI精讲:Tabs 标签页组件的实现

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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。