Tabbar是咱们日常开发中常常运用到的组件,然而在SwiftUI中,Tabbar目前只要TabView有相关的完结,这显然是不符合咱们日常开发的需求的,所以让咱们一同看看如何完结自定义的Tabbar吧~

1.运用TabView完结

1-1:文档检查

要用TabView前,咱们老规矩,先看看文档,如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)
咱们经过文档给咱们供给的文字描述以及相关例子,咱们能够知道 TabView 是配合着 .tabItem 润饰符进行运用的。有仔细看文档的小伙伴,能发现文档里面描述了这么一句话:

Use aLabelfor each tab item, or optionally aText, anImage, or an image followed by text. Passing any other type of view results in a visible but empty tab item.

大致翻译过来便是:你别看这个 .tabItem 润饰符的传参是符合View协议的,但可不是你传啥我就给你显示啥。我这儿只接纳 Label 控件,或许传入一个 Text 控件 和 Image 控件。

1-2:代码完结

依据上方大致的描述,咱们能够轻松地写出以下代码:

struct ContentView:View{
    @State var currentSelectd: Int = 1
    struct TabItem{
        var id:Int
        var text:String
        var icon:String
    }
    let tabItems = [
        TabItem(id:1,text:"主页",icon:"book"),
        TabItem(id:2,text:"地址",icon:"location"),
        TabItem(id:3,text:"保藏",icon:"heart"),
        TabItem(id:4,text:"我的",icon:"person"),
    ]
    var body:some View{
        VStack{
            Text("当时触发的是:\(currentSelectd)")
            TabView(selection: $currentSelectd) {
                ForEach(tabItems,id:\.id){ item in
                    Text(item.text).tabItem{
                        Label(item.text, systemImage: item.icon)
                       // 下面这种写法同样生效
//                        VStack{
//                            Text(item.text).foregroundColor(.red)
//                            Image(systemName: item.icon)
//                        }
                    }
                }
            }
        }
    }
}

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

有的朋友可能会问,为啥你的 .tabItem 后面,不必跟 .tag 润饰符去给视图设置仅有值呢?嘿嘿,咱们来看看 .tag 的文档,它是这么描述的:

AForEachautomatically applies a default tag to each enumerated view using theidparameter of the corresponding element.

呜呼~文档告知咱们 ForEach 运用相应元素的 id 参数会自动标记并应用于每个视图。

那么上方的代码,还有能够优化的点吗?答案是:有的。

struct ContentView:View{
    @State var currentSelectd: Int = 1
    struct TabItem:Identifiable{
        var id:Int
        var text:String
        var icon:String
    }
    let tabItems = [
        TabItem(id:1,text:"主页",icon:"book"),
        TabItem(id:2,text:"地址",icon:"location"),
        TabItem(id:3,text:"保藏",icon:"heart"),
        TabItem(id:4,text:"我的",icon:"person"),
    ]
    var body:some View{
        VStack{
            Text("当时触发的是:\(currentSelectd)")
            TabView(selection: $currentSelectd) {
                ForEach(tabItems){ item in
                    Text(item.text).tabItem{
                        Label(item.text, systemImage: item.icon)
                    }
                }
            }
        }
    }
}

咱们修正结构体 TabItem,使其符合名为 Identifiable 的新协议。这样做有什么好处呢?咱们能够看到,我把 ForEach 中的 id 参数给删除了。这便是它的好处,咱们不再需求告知 ForEach 运用哪个特点作为仅有的标识符。

1-3:款式调整

尽管 TabView 的款式自定制比较鸡肋,但咱们仍是能够略微改点款式的,比方咱们希望修正 TabView 被成功激活后的颜色,咱们能够运用 .tink 润饰符,如下所示:

TabView(selection: $currentSelectd) {
    ForEach(tabItems){ item in
        Text(item.text).tabItem{
            Label(item.text, systemImage: item.icon)
        }
    }
}
.tint(.pink)

款式如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

假如咱们想躲藏下方的 tabbar 栏,经过手势滑动来切换视图,能够运用 .tabViewStyle 润饰符,如下所示:

TabView(selection: $currentSelectd) {
    ForEach(tabItems){ item in
        Text(item.text).tabItem{
            Label(item.text, systemImage: item.icon)
        }
    }
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

假如想保存 tabbar 的图标并支持手势滑动的话,能够运用 .indexViewStyle 润饰符,如下所示:

TabView(selection: $currentSelectd) {
    ForEach(tabItems){ item in
        Text(item.text).tabItem{
            Label(item.text, systemImage: item.icon)
        }
    }
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

在感受到 TabView 如此”强壮”自定义款式功能后,信任不少人现已跟我一样,内心缓缓说出两个字:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

2.自定义 Tabbar 组件

2-1: 新建相关目录结构

咱们新建 Views 文件夹,在里面新建4个视图文件,并简单的将视图名作为 Text 的输入值即可,如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

接着咱们新建 Model 文件夹,在里面新建 Tab 文件,并写入以下代码:

enum Tab: CaseIterable{
    case home
    case location
    case collect
    case mine
    var text:String{
        switch self{
        case .home:
            return "主页"
        case .location:
            return "地址"
        case .collect:
            return "保藏"
        case .mine:
            return "我的"
        }
    }
    var icon:String{
        switch self{
        case .home:
            return "book"
        case .location:
            return "location"
        case .collect:
            return "heart"
        case .mine:
            return "person"
        }
    }
}

关于CaseIterable,没用过的朋友能够点击检查文档哦。这是目前我以为比较简练的方法了~在从前咱们定义 Struct TabItem,现在能够不必啦。代码总是越写越好的,你觉得呢?

接着咱们新建Components文件夹,在里面新增tabbar文件。至此,咱们的目录结构如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

咱们有没有觉得很熟悉,在前端的工程化项目中,也有相似的结构。(由于我便是前端)

2-2: tabbar 组件完结

在前端的组件完结中, tabbar组件通常是单独抽出来的,咱们通常会在各大组件库中,找到不错的完结。今日咱们也来完结一下 SwiftUI 版别的。首先,咱们遍历枚举Tab,渲染出相关元素。代码如下:

struct tabbar: View {
    @State var currentSelected: Tab = .home
    var body: some View {
        HStack{
            ForEach(Tab.allCases, id: \.self) { tabItem in
                Button{
                    currentSelected = tabItem
                } label:{
                    VStack{
                        Image(systemName: tabItem.icon)
                        Text(tabItem.text)
                    }
                }
            }
        }
    }
}

作用图如下:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

接下来,咱们加上一些想要的款式,包括选中后的图标款式,全体布景色等,代码如下:

struct tabbar: View {
    @State var currentSelected: Tab = .home
    var body: some View {
        HStack{
            ForEach(Tab.allCases, id: \.self) { tabItem in
                Button{
                    currentSelected = tabItem
                } label:{
                    VStack{
                        Image(systemName: tabItem.icon)
                            .font(.system(size: 24))
                            .frame(height: 30)
                        Text(tabItem.text)
                            .font(.body.bold())
                    }
                    .foregroundColor(currentSelected == tabItem ? .pink : .secondary)
                    .frame(maxWidth: .infinity)
                }
            }
        }
        .padding(6)
        .background(.white)
        .cornerRadius(10)
        .shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 4)
        .padding(.horizontal)
    }
}

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

哎呀,这看着不过瘾呐。假如我还想要点击的时分有布景色,并且布景色在点击的时分,要有移动的过渡作用,这怎么办呢?

2-3:withAnimation 与 matchedGeometryEffect

在SwiftUI中,若要运用动画,咱们能够运用到 withAnimation,咱们先写个小例子来感受一下:

struct ContentView:View{
    @State var distance: CGFloat = -100
    var body:some View{
        VStack {
            Button{
                withAnimation(.easeOut(duration: 1)){
                    distance = 100
                }
            } label: {
                Text("点击触发动画")
            }
            Rectangle().fill(.pink).frame(width: 100,height: 100).offset(x:distance)
        }
    }
}

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

matchedGeometryEffect 则是 ios14 版别出的润饰符,咱们来看看它的参数是怎么传的。

func matchedGeometryEffect<ID>(
    id: ID,
    in namespace: Namespace.ID,
    properties: MatchedGeometryProperties = .frame,
    anchor: UnitPoint = .center,
    isSource: Bool = true
) -> some View where ID : Hashable
  • id: 由于该方法能够同步不同视图组的几许图形,id 参数能够让咱们对它们进行相应的分组。它能够是任何 Hashable 类型(例如,Int、String)
  • namespace: 为了避免 id 抵触,两个视图的配对由 id + namespace确认。
  • properties: 要从源视图仿制的特点。什么是源视图?isSource = true便是源视图了,那么properties会用在非源视图中。源视图一直同享其所有几许图形(size、position),该参数默认值为 .frame ,意味着它同时匹配着 size 和 position。咱们能够在非源视图中,经过 properties:.size/.position来指定要从源视图仿制的特点。
  • anchor: 视图中用于生成其同享方位值的相对方位。
  • isSource:默以为 true, 视图将被应用为其他视图的几许源。

关于 matchedGeometryEffect ,咱们在本篇内容只会用到 id + namespace,所以咱们的心理负担不必太重。

2-4:Tabbar布景添加过渡作用

经过以上的了解,咱们能够对之前的代码,稍作修正:

struct tabbar: View {
    @State var currentSelected: Tab = .home
    @Namespace var animationNamespace
    var body: some View {
        HStack{
            ForEach(Tab.allCases, id: \.self) { tabItem in
                Button{
                    withAnimation(.easeInOut) {
                        currentSelected = tabItem
                    }
                } label:{
                    VStack{
                        Image(systemName: tabItem.icon)
                            .font(.system(size: 24))
                            .frame(height: 30)
                        Text(tabItem.text)
                            .font(.body.bold())
                    }
                    .foregroundColor(currentSelected == tabItem ? .pink : .secondary)
                    .frame(maxWidth: .infinity)
                    // 新增布景过渡作用
                    .background(
                        ZStack{
                            if currentSelected == tabItem {
                                RoundedRectangle(cornerRadius: 10)
                                    .fill(.pink.opacity(0.2))
                                .matchedGeometryEffect(id: "background_rectangle", in: animationNamespace)
                            }
                        }
                    )
                }
            }
        }
        .padding(6)
        .background(.white)
        .cornerRadius(10)
        .shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 4)
        .padding(.horizontal)
    }
}

作用如图所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

咦,尽管作用是咱们想要的,可是为什么往回点的时分,没有相应的过渡动画呢?原因是 Xcode 的preview,有时分并不能很好的出现相关的动画作用。咱们能够按下 command + R,启动 Simulator 来看看作用:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

这样看起来正常多了~可是动画看起来很普通,我想让它在过渡过程中添加一些 “弹性” 的作用,那要怎么做呢?

咱们能够在 withAnimation 中,添加 .spring 润饰符,如下所示:

withAnimation(.spring(response: 0.3,dampingFraction: 0.7)) {
  currentSelected = tabItem
}

作用如下:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

怎么样,动画作用是不是更加流畅了~

2-5:完善 Tabbar

在上方的代码中,咱们现已做出了一个大致的 Tabbar 组件了~但仍是有问题,正常的tabbar是置于底部的,咱们需求把它先放到页面的底部去,如下所示:

HStack{...}
.padding(6)
.background(.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 4)
.padding(.horizontal)
// 新增
.frame(maxHeight: .infinity,alignment: .bottom)

假如你的tabbar需求是贴着底部的话,你能够加上 .ignoresSafeArea() 润饰符,作用是疏忽iphone的安全区域。

接着咱们把 tabbar 组件放到 contentView 中运用。

struct ContentView:View{
  var body:some View{
    VStack {
      tabbar()
    }
  }
}

这样就完结了吗?答案是:No。有web开发经验的朋友们应该能感觉到,之前咱们在web端用tabbar组件时,父组件是需求知道tabbar组件当时切换的状况的,一般在web端,咱们都会经过Vue的emit,或许在React中调用父组件传过来的callback函数,做到让父组件知晓这个tabbar的即时状况。那么在SwiftUI中,咱们应该如何去做呢?

咱们能够运用 @Binding 润饰器,在 tabbar 组件中,咱们将 currentSelected 的 @State 润饰器改为 @Binding,如下所示:

@State var currentSelected: Tab = .home // 旧
@Binding var currentSelected: Tab // 新

同时咱们也能够把下方的 tabbar_Previews 注释掉,由于咱们现已不需求了~ 接着咱们在 ContenView 中,将代码改为:

struct ContentView:View{
    @State private var currentSelected:Tab = .location
    var body:some View{
        VStack {
            switch currentSelected {
            case .home:
                HomeView()
            case .location:
                LocationView()
            case .collect:
                CollectView()
            case .mine:
                MineView()
            }
            tabbar(currentSelected:$currentSelected)
        }
    }
}

这样咱们就能完结依据不同的tab切换到不同的View啦,作用如下所示:

SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果)

至此,功德圆满,咱们现已完结了一个不错的tabbar组件~

感谢您的阅读,欢迎批评与纠正,或在谈论区进行沟通~