开启生长之旅!这是我参加「日新方案 12 月更文应战」的第6天,点击检查活动详情

好久不见,我是 new_cheng。

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

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

  1. 更灵敏的操控 TabView 的显现;
  2. 高度的定制化,比方给 TabView 设置皮肤(谁能不爱美观的皮肤呢?);

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

话不多说,开搞。

完成思路

一个规范的 TabView, 先来看看完成图:

SwiftUI 开发之旅:自定义 TabView

完成思路也很简略:

  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保证目标只会被创立一次。因而,假如是在视图里自行创立的ObservableObjectmodel 目标,运用@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 就有很多漂亮的皮肤,这对提高用户粘性来说,是一个不错的方式(含泪给王者农药打钱)。当然这彻底是由你或者设计师来决议的,按照设计稿开搞便是了。

SwiftUI 开发之旅:自定义 TabView

关于怎么做 SwiftUI 的皮肤定制,先给自己挖个坑,以后来填上。

额定收成

当运用 swiftui 的 NavigationView 进行导航时,假如你在一个父级视图中运用了 NavigationView,然后在子级视图中也运用了 NavigationView,那在进行导航的时候,就有或许出现 2 个导航栏的状况。

SwiftUI 开发之旅:自定义 TabView

这时候的解决办法是便是仅在尖端父视图,也便是 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,经过她,咱们能做到高度的自定义作用,值得一试!

这是 SwiftUI 开发之旅专栏的文章,是 swiftui 开发学习的经验总结及实用技巧分享,欢迎关注该专栏,会坚持输出。同时欢迎关注我的个人大众号 @JSHub:供给最新的开发信息速报,优质的技能干货引荐。或是检查我的个人博客:Devcursor。

点赞:假如有收成和帮助,请点个赞支撑一下!

保藏:欢迎保藏文章,随时检查!

评论:欢迎评论交流学习,共同进步!