阅读 67

SwiftUI 开发之旅:自定义 TabView

好久不见,我是 new_cheng。

关于自定义 TabView,首先要明白,为什么不使用官方的 TabView,为什么要自定义一个 TabView?

有几个值得这么做的理由:

  1. 更灵活的控制 TabView 的显示;

  2. 高度的定制化,比如给 TabView 设置皮肤(谁能不爱好看的皮肤呢?);

以上 2 点就值得你自定义一个 TabView。

话不多说,开搞。

实现思路

一个标准的 TabView, 先来看看完成图:

image.png

实现思路也很简单:

  1. 创建一个路由控制器;

  2. 创建一个 TabBarIcon;

  3. 自定义视图

  4. 检测路由变化,切换视图;

创建路由控制器

这里的路由只是一个称呼,和前端领域的路由不同。

ViewRouter.swift:

import SwiftUI enum Page {     case home     case my } class ViewRouter: ObservableObject {     @Published var currentPage: Page = .home } 复制代码

ViewRouter 是一个遵循 ObservableObject 协议的类,它的 currentPage 属性是用 @Published 进行包装的。当属性值发生变化时,使用该类的任何视图都会自动重新调用 body 属性,来保持界面与数据的一致性。

创建 TabBarIcon

接着,创建 TabView 的菜单内容,我们得封装一个 TabBarIcon

ViewRouter.swift 文件中新增以下代码:

struct TabBarIcon: View {     @StateObject var viewRouter: ViewRouter     let assignedPage: Page     let width, height: CGFloat     let systemIconName, tabName: String     var body: some View {         VStack {             Image(systemName: systemIconName)                 .resizable()                 .aspectRatio(contentMode: .fit)                 .frame(width: width, height: height)                 .padding(.top, 6)             Text(tabName)                 .font(.footnote)                 .font(.system(size: 16))             Spacer()         }         .padding(.horizontal, -2)         .onTapGesture {             viewRouter.currentPage = assignedPage         }         .foregroundColor(viewRouter.currentPage == assignedPage ? .blue : .gray)     } } 复制代码

当用户点击的时候,会去更新当前路由;值得一提的是,这里我们采用的是 @StateObject,而不是 @ObservedObject,@ObservedObject 不管存储,会随着视图的创建被多次创建。而 @StateObject 保证对象只会被创建一次。因此,如果是在视图里自行创建的 ObservableObject model 对象,使用 @StateObject 会是更正确的选择。

自定义视图

为了显示路由视图,我们还需要自定义视图。借助 GeometryReader,我们可以很轻松的做到这一点。GemoetryReader 是一个容器视图,能够根据其自身大小和坐标空间定义其内容,简单来说,GeometryReader 是一种特别的 View,在其中可以拿到一些你在其他 View 中拿不到的信息,比如父级视图的 size。

ContentView.siwft:

struct ContentView: View {     var body: some View {         GeometryReader { geometry in             VStack {                 Spacer()                 ZStack {                     HStack {                         TabBarIcon(viewRouter: viewRouter, assignedPage: .home,                          width: geometry.size.width/5, height: geometry.size.height/32,                          systemIconName: "chart.pie.fill", tabName: "首页")                             .frame(maxWidth: .infinity)                         TabBarIcon(viewRouter: viewRouter, assignedPage: .my,                         width: geometry.size.width/5, height: geometry.size.height/32,                          systemIconName: "person.crop.circle.fill", tabName: "我的")                             .frame(maxWidth: .infinity)                     }                     // 将宽度设置为父视图的宽度大小,高度需要微调,可以设置为具体是数值,比如 100                     .frame(width: geometry.size.width, height: geometry.size.height/9)                 }             }             .ignoresSafeArea(edges: .bottom)         }     } } 复制代码

检测路由变化,切换视图

switch 来切换视图:

struct ContentView: View {     @StateObject var viewRouter: ViewRouter     var body: some View {         GeometryReader { geometry in             VStack {                 switch viewRouter.currentPage {                     case .home:                         Home()                     case .my:                         My()                 }                 Spacer()                 ZStack {                     HStack {                         TabBarIcon(viewRouter: viewRouter, assignedPage: .home,                          width: geometry.size.width/5, height: geometry.size.height/32,                          systemIconName: "chart.pie.fill", tabName: "首页")                             .frame(maxWidth: .infinity)                         TabBarIcon(viewRouter: viewRouter, assignedPage: .my,                         width: geometry.size.width/5, height: geometry.size.height/32,                          systemIconName: "person.crop.circle.fill", tabName: "我的")                             .frame(maxWidth: .infinity)                     }                     // 将宽度设置为父视图的宽度大小,高度需要微调,可以设置为具体是数值,比如 100                     .frame(width: geometry.size.width, height: geometry.size.height/9)                 }             }             .ignoresSafeArea(edges: .bottom)         }     } } // 设置预览 struct ContentView_Previews: PreviewProvider {     static var previews: some View {         ContentView(viewRouter: ViewRouter())     } } 复制代码

到这里,一个自定义 TabView 就完成了。

皮肤

我们的 TabView 既然都是完全自定义的了,那给它开发皮肤自然不在话下了;像招行的 APP 就有很多漂亮的皮肤,这对提高用户粘性来说,是一个不错的方式(含泪给王者农药打钱????)。当然这完全是由你或者设计师来决定的,按照设计稿开搞就是了。

image.png

额外收获

当使用 swiftui 的 NavigationView 进行导航时,如果你在一个父级视图中使用了 NavigationView,然后在子级视图中也使用了 NavigationView,那在进行导航的时候,就有可能出现 2 个导航栏的情况。

image.png

这时候的解决办法是就是仅在顶级父视图,也就是 ContentView.swift 中使用 NavigationView:

struct ContentView: View {     @StateObject var viewRouter: ViewRouter     var body: some View {         NavigationView {             // ...         }     } } 复制代码

此时,如果你使用的是 swiftui 自带的 TabView 视图,而非自定义的,而又想在子视图中不显示 TabView,这会很麻烦,需要借助第三方库:SwiftUI-Introspect。

到这,还没有完,用 SwiftUI-Introspect 控制官方 TabView 的显示,显示和隐藏的时候,会有一个过渡动画...

如果你不介意在每次从子视图返回首页的时候,TabView 显示的这个过度动画带来的延迟效果,那就可以采用官方的 TabView。

而当你使用自定义的 TabView 时,这些问题都迎刃而解????。

总结

我们用简单的代码就实现了一个自定义的 TabView,通过她,我们能做到高度的自定义效果,值得一试!


作者:new_cheng
链接:https://juejin.cn/post/7171233863510245412

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐