如何从头开始创建可滑动的 UITabBar

已发表: 2022-03-11

如您所知,Apple 的 iOS SDK 包含无数的内置 UI 组件。 按钮、容器、导航、选项卡式布局,应有尽有——几乎所有你需要的东西都在那里。 或者是吗?

所有这些基本组件都允许我们创建基本的结构化 UI,但如果需要跳出框框会发生什么? 当 iOS 开发人员需要构建 SDK 默认不支持的某种行为时?

其中一种情况是UITabBar ,您无法在选项卡之间滑动,也没有用于在选项卡之间切换的动画。

寻找一个简单的 UITabBar 修复

经过大量搜索,我在 Github 上只找到了一个有用的库。 不幸的是,该库在运行应用程序时产生了很多问题,尽管乍一看它似乎是一个优雅的解决方案。

换句话说,我发现这个库很容易使用,但是有问题,这显然超过了它的易用性,而且往往会导致问题。 如果您仍然感兴趣,可以在此链接下找到该库。

所以,经过一番思考和大量搜索,我开始实现自己的解决方案,我对自己说: “嘿,如果我们使用页面视图控制器进行滑动和原生 UITabBar 会怎样。 如果我们将这两个东西组合在一起,在滑动或点击标签栏时处理页面索引呢?”

最终,我想出了一个解决方案,尽管它被证明有些棘手,我稍后会解释。

精心设计的 UITabBar 解决方案

假设您要构建三个选项卡项,这自动意味着每个选项卡项要显示三个页面/控制器。

在这种情况下,您将需要实例化这三个视图控制器,并且您还需要两个占位符/空视图控制器用于选项卡栏,以制作选项卡栏项目,在按下选项卡时更改它们的状态,或者当用户想要更改时以编程方式设置选项卡索引。

为此,让我们深入研究 Xcode 并编写几个类,看看这些东西是如何工作的。

在选项卡之间滑动的示例

iOS 中可滑动标签的示例

在这些截图中,你可以看到第一个标签栏项目是蓝色的,然后用户滑动到右边的标签,这是黄色的,最后一个屏幕显示第三个项目被选中,所以整个页面显示为黄色。

可滑动标签栏的编程使用

因此,让我们深入研究这个功能,并为 iOS 编写一个可滑动标签栏的简单示例。 首先,我们需要创建一个新项目。

我们项目所需的先决条件非常基本:在 Mac 上安装 Xcode 和 Xcode 构建工具。

要创建一个新项目,请在 Mac 上打开 Xcode 应用程序并选择“创建一个新的 Xcode 项目”,然后为您的项目命名,最后选择要创建的应用程序的类型。 只需选择“单一视图应用程序” ,然后按下一步。

Xcode 截图

如您所见,下一个屏幕将要求您提供一些基本信息:

  • 产品名称:我将它命名为SwipeableTabbar。
  • 团队:如果你想在真实设备上运行这个应用程序,你必须有一个开发者帐户。 就我而言,我将为此使用自己的帐户。

注意:如果您没有开发者帐户,您也可以在模拟器上运行它。

  • 组织名称:我将其命名为Toptal
  • 组织标识符:我将其命名为com.toptal
  • 语言:选择 Swift。
  • 取消选中: “使用核心数据”、“包括单元测试”“包括 UI 测试”。

按下一步按钮,您就可以开始构建可滑动的标签栏了。

简单的架构

正如您现在已经知道的那样,当您创建一个新应用程序时,您已经拥有了 Main ViewController类和Main.Storyboard

在我们开始设计之前,让我们首先创建所有必要的类和文件,以确保在我们继续工作的 UI 部分之前我们已经设置并运行了所有内容。

在您的项目中的某个地方,只需创建一些新文件 -> TabbarController.swiftNavigationController.swiftPageViewController.swift

就我而言,它看起来像这样。

截图:Xcode 控制器

AppDelegate文件中,只保留didFinishLaunchingWithOptions ,因为您可以删除所有其他方法。

didFinishLaunchingWithOptions中,只需复制并粘贴以下行:

 window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true

从名为ViewController.swift的文件中删除所有内容。 我们稍后会回到这个文件。

首先,让我们为NavigationController.swift编写代码。

 import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }

有了这个,我们刚刚创建了一个简单的UINavigationController ,其中我们有一个带有灰色 TintColor 的半透明条。 就在这里。

现在,我们可以继续使用PageViewController了。

在其中,我们需要比之前讨论的文件多编写一些代码。

该文件包含一个类、一个协议、一些UIPageViewController数据源和委托方法。

生成的文件需要如下所示:

Xcode 截图:方法

如您所见,我们已经声明了我们自己的协议,称为PageViewControllerDelegate ,它应该告诉标签栏控制器在处理滑动后页面索引已更改。

 import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }

然后我们需要创建一个名为PageViewController的新类,它将保存我们的视图控制器,选择特定索引处的页面,并处理滑动。

假设我们第一次运行时选择的第一个控制器应该是中心视图控制器。 在这种情况下,我们分配我们的默认索引值,等于 1。

 class PageViewController: UIPageViewController { weak var swipeDelegate: PageViewControllerDelegate? var pages = [UIViewController]() var prevIndex: Int = 1 override func viewDidLoad() { super.viewDidLoad() self.dataSource = self self.delegate = self } func selectPage(at index: Int) { self.setViewControllers( [self.pages[index]], direction: self.direction(for: index), animated: true, completion: nil ) self.prevIndex = index } private func direction(for index: Int) -> UIPageViewController.NavigationDirection { return index > self.prevIndex ? .forward : .reverse } }

正如您在此处看到的,我们有变量pages ,其中将包含我们所有视图控制器的引用。

变量prevIndex用于存储最后选择的索引。

您可以简单地调用selectPage方法来设置选定的索引。

如果你想监听页面索引的变化,你必须订阅swipeDelegate ,并且在每次页面滑动时,你会收到页面索引改变的通知,另外你也会收到当前的索引。

方法 direction 将返回UIPageViewController的滑动方向。 此类中的最后一块拼图是委托/数据源实现。

幸运的是,这些实现非常简单。

 extension PageViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let previousIndex = viewControllerIndex - 1 guard previousIndex >= 0 else { return nil } guard pages.count > previousIndex else { return nil } return pages[previousIndex] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let nextIndex = viewControllerIndex + 1 guard nextIndex < pages.count else { return nil } guard pages.count > nextIndex else { return nil } return pages[nextIndex] } } extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed { guard let currentPageIndex = self.viewControllers?.first?.view.tag else { return } self.prevIndex = currentPageIndex self.swipeDelegate?.pageDidSwipe(to: currentPageIndex) } } }

正如您在上面看到的,有三种方法在起作用:

  • 第一个找到索引并返回前一个视图控制器。
  • 第二个找到索引并返回下一个视图控制器。
  • 最后一个检查滑动是否结束,将当前索引设置为本地属性prevIndex ,然后调用委托方法通知父视图控制器滑动成功结束。

现在我们终于可以编写我们的UITabBarController实现了:

 import UIKit class TabbarController: UITabBarController { let selectedColor = UIColor.blue let deselectedColor = UIColor.gray let tabBarImages = [ UIImage(named: "ic_music")!, UIImage(named: "ic_play")!, UIImage(named: "ic_star")! ] override func viewDidLoad() { view.backgroundColor = .gray self.delegate = self tabBar.isTranslucent = true tabBar.tintColor = deselectedColor tabBar.unselectedItemTintColor = deselectedColor tabBar.barTintColor = UIColor.white.withAlphaComponent(0.92) tabBar.itemSpacing = 10.0 tabBar.itemWidth = 76.0 tabBar.itemPositioning = .centered setUp() self.selectPage(at: 1) } }

如您所见,我们创建了具有默认属性和样式的TabbarController 。 我们需要为选中和取消选中的条形项目定义两种颜色。 此外,我还为标签栏项目介绍了三个图像。

viewDidLoad中,我只是设置标签栏的默认配置并选择第 1 页。 这意味着启动页面将是第一页。

 private func setUp() { guard let centerPageViewController = createCenterPageViewController() else { return } var controllers: [UIViewController] = [] controllers.append(createPlaceholderViewController(forIndex: 0)) controllers.append(centerPageViewController) controllers.append(createPlaceholderViewController(forIndex: 2)) setViewControllers(controllers, animated: false) selectedViewController = centerPageViewController } private func selectPage(at index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) guard let PageViewController = (self.viewControllers?[1] as? PageViewController) else { return } PageViewController.selectPage(at: index) }

在 setUp 方法中,您会看到,我们创建了两个占位符视图控制器。 UITabBar需要这些占位符控制器,因为标签栏项目的数量必须等于您拥有的视图控制器的数量。

如果您还记得,我们使用UIPageViewController来显示控制器,但是对于UITabBar ,如果我们想让它完全可用,我们需要实例化所有视图控制器,以便当您点击它们时栏项目将起作用。 因此,在本例中, placeholderviewcontroller #0#2是空视图控制器。

作为居中的视图控制器,我们创建了一个具有三个视图控制器的PageViewController

 private func createPlaceholderViewController(forIndex index: Int) -> UIViewController { let emptyViewController = UIViewController() emptyViewController.tabBarItem = tabbarItem(at: index) emptyViewController.view.tag = index return emptyViewController } private func createCenterPageViewController() -> UIPageViewController? { let leftController = ViewController() let centerController = ViewController2() let rightController = ViewController3() leftController.view.tag = 0 centerController.view.tag = 1 rightController.view.tag = 2 leftController.view.backgroundColor = .red centerController.view.backgroundColor = .blue rightController.view.backgroundColor = .yellow let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) guard let pageViewController = storyBoard.instantiateViewController(withIdentifier: "PageViewController") as? PageViewController else { return nil } pageViewController.pages = [leftController, centerController, rightController] pageViewController.tabBarItem = tabbarItem(at: 1) pageViewController.view.tag = 1 pageViewController.swipeDelegate = self return pageViewController } private func tabbarItem(at index: Int) -> UITabBarItem { return UITabBarItem(title: nil, image: self.tabBarImages[index], selectedImage: nil) }

上面描述的第一个和第二个方法是我们的​​页面浏览控制器的 init 方法。

tabbar item 方法只返回索引处的tabbar项目。

如您所见,在createCenterPageViewController()中,我为每个视图控制器使用标签。 这有助于我了解屏幕上出现了哪个控制器。

接下来,我们来看看可能是我们最重要的方法, handleTabbarItemChange

 private func handleTabbarItemChange(viewController: UIViewController) { guard let viewControllers = self.viewControllers else { return } let selectedIndex = viewController.view.tag self.tabBar.tintColor = selectedColor self.tabBar.unselectedItemTintColor = selectedColor for i in 0..<viewControllers.count { let tabbarItem = viewControllers[i].tabBarItem let tabbarImage = self.tabBarImages[i] tabbarItem?.selectedImage = tabbarImage.withRenderingMode(.alwaysTemplate) tabbarItem?.image = tabbarImage.withRenderingMode( i == selectedIndex ? .alwaysOriginal : .alwaysTemplate ) } if selectedIndex == 1 { viewControllers[selectedIndex].tabBarItem.selectedImage = self.tabBarImages[1].withRenderingMode(.alwaysOriginal) } }

在这种方法中,我使用视图控制器作为参数。 从这个视图控制器,我得到一个标签作为选定的索引。 对于标签栏,我们需要设置选中和未选中的颜色。

现在我们需要遍历所有控制器并检查i == selectedIndex

然后我们需要将图像渲染为原始渲染模式,否则我们需要将图像渲染为模板模式

当您使用模板模式渲染图像时,它将从项目的色调颜色继承颜色。

我们快完成了。 我们只需要从UITabBarControllerDelegatePageViewControllerDelegate中引入两个重要的方法。

 func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { self.selectPage(at: viewController.view.tag) return false } func pageDidSwipe(to index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) }

当您按下任何选项卡项目时调用第一个,而当您在选项卡之间滑动时调用第二个。

包起来

当您将所有代码放在一起时,您会注意到您不必编写自己的手势处理程序实现,也不必编写大量代码来在标签栏项目之间提供平滑的滚动/滑动。

这里讨论的实现并不适合所有场景,但它是一个古怪、快速且相对简单的解决方案,使您能够使用少量代码创建这些功能。

最后,如果您想尝试我的方法,可以使用我的 GitHub 存储库。 快乐编码!