Swift 如何实现自定义 Tab Bar

前言

每个 UI 设计师都喜欢美丽而有动画效果的 Tab Bar。然而,对于开发人员来说,实现这种设计可能是一场噩梦。当然,使用 Apple 的原生 Tab Bar 组件并专注于更有趣的事情,比如业务逻辑的实现,会更容易。但如果我们必须创建自定义 Tab Bar 该如何实现呢?

介绍实现流程

在本文中,将尝试回答这些问题。我们将介绍创建自定义 Tab Bar 的最重要方面。最终效果将是一个具有动画效果、易于扩展、完全自定义的 Tab Bar,希望它能为你节省将来的时间,使设计师梦寐以求的 Tab Bar 的实现更快捷和更舒适。下面来介绍一下实现流程:

首先,需要说明的是,本示例使用了一些依赖项。借助这些依赖项,更容易实现本文中描述的解决方案。随时将它们替换为你的本机代码或其他库。这些依赖关系包括:

  • 用于布局的 SnapKit
  • 用于处理 Tab Bar 项上的点击操作的 RxGesture
  • 用于通知 TabBarController 选中项目的 RxSwift

现在,让我们来实现这个功能。

Tab Bar 初步介绍

Tab Bar 项组件由两个不同的部分组成:视图层和处理所有定义的项目类型的枚举。

为什么使用枚举类型而不是简单的结构?

这很简单,枚举在编程中特别是在 Swift 语言中是强大的类型。它们将熟悉的情况汇总在一起,可迭代,可以扩展以展开有关特定情况的信息,或者可以在其中使用计算属性。

此外,枚举与结构一样是值类型。

enum CustomTabItem: String, CaseIterable {
    case profile
}

extension CustomTabItem {
    var viewController: UIViewController { }
    var icon: UIImage? { }
    var selectedIcon: UIImage? { }
    var name: String { }
}

根据上面的代码可以看出,CustomTabItem 具有用于图标、选定图标、名称和关联 viewController 的属性。每个值都使用 switch 语句定义,为每个枚举情况返回特定值。

第二部分是 Tab Bar 项的视图层。它直接与 CustomTabItem 关联,因为 Tab Bar 项是初始化期间传递给视图的两个参数之一。第二个参数是每个视图唯一的索引,稍后将用于更改 Tab Bar 的 selectedIndex

此外,每个视图都具有简单的过渡动画。项目的设计很简单,包括顶部的图标和下面的标题。根据项目是否已选择,标题可以变成图标下面的圆线。

Tab Bar实现

让我们聊聊如何将所有 Tab Bar 项汇总并处理其选择的主要组件。自动布局非常容易,因为它是 UIStackView 组件的子类。唯一需要做的事情就是使用 addArrangedSubview(_ view: UIView) 方法添加我们的项目。这是在 setupHierarchy() 方法中处理的。

GitHub 存储库的 Extensions.swift 文件中,可以找到一些有用的扩展,比如用一行代码向 UIStackView 添加许多子视图的扩展。

此外,对于要添加到 Tab Bar 的每个项目,我们必须将 translatesAutoresizingMaskIntoConstraints 设置为 false,以防止自动创建它们的自动布局,并将 clipsToBounds 设置为 true,以将我们的项目剪切到 Tab Bar 的边界。我们将在 setupProperties() 方法中实现这一部分,以及其他属性配置。

final class CustomTabBar: UIStackView {
    
    var itemTapped: Observable<Int> { itemTappedSubject.asObservable() }
    
    private let itemTappedSubject = PublishSubject<Int>()
    private let disposeBag = DisposeBag()
    
    init() { }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

除了Tab Bar项的声明之外,我们还必须添加负责发出所选项目索引的 Observable 序列属性。

正如上面所示,主体本身声明为私有常量,并且具有关于主题的值的 Observable。

由于这样做,我们可以确保没有人能够干扰从 CustomTabBar 类外部发出的该主题的值。

最后但同样重要的是,我们必须添加两个方法到我们的 CustomTabBar 实现中。第一个是 selectItem(index: Int),在这里,我们将处理更新当前所选项目的所有逻辑。

首先,我们更新每个项目中的 isSelected 属性,以反映最新的选择。

其次,我们使用上面描述的主题发出所选项目的索引。通过这种方式,我们同时在 TabBarController 和 Tab Bar 项中更新选择。核心代码如下:

private func selectItem(index: Int) {
    customItemViews.forEach { $0.isSelected = $0.index == index }
    itemTappedSubject.onNext(index)
}

我们必须实现的第二个方法是 bind(),负责处理用户对我们的每个项目的触摸。

实现使用 RxGesture,但如果你愿意,可以用你自己的 Reactive 扩展替换它。RxGesture 提供了一种 .tapGesture() 方法,用于识别用户交互,但在绑定到该操作之前,我们必须过滤用户手势,仅选择具有已识别状态的手势。核心代码如下:

private func bind() {
    profileItem.rx.tapGesture()
        .when(.recognized)
        .bind { [weak self] _ in
            guard let self = self else { return }
            self.profileItem.animateClick {
                self.selectItem(index: self.profileItem.index)
            }
        }
        .disposed(by: disposeBag)
}

如上面代码,animateClick(completion: @escaping () -> Void) 的闭包内部调用了 selectItem(index: Int) 方法。这是一个 UIView 的扩展,用于缩放动画我们的项目。

上面的代码示例介绍了如何处理 profileItem 的用户交互。但不要忘记以相同的方式处理其他相关的组件!

布局在 Tab Bar Controller 中

到目前为止,前期工作都已完成,现在我们必须将所有部分组合在一起!我们可以使用 CustomTabBarController 来完成,但首先,我们需要一个用于处理与 Tab Bar 项目相关的不同屏幕的简单视图控制器。代码如下:

class ViewController: UIViewController {
    
    private let titleLabel = UILabel()
    private let item: CustomTabItem
    
    init(item: CustomTabItem) {
        self.item = item
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

上面的示例故意省略了自动布局和属性配置,因为这是一个简单的添加。每个视图控制器都使用 CustomTabItem 进行初始化,因此我们可以在屏幕上显示项目的名称。

回到 Tab Bar 控制器,除了层次结构和布局设置之外,我们必须配置一些属性。代码如下:

private func setupProperties() {
    tabBar.isHidden = true
        
    customTabBar.translatesAutoresizingMaskIntoConstraints = false
    customTabBar.addShadow()
        
    selectedIndex = 0
    let controllers = CustomTabItem.allCases.map { $0.viewController }
    setViewControllers(controllers, animated: true)
}

首先,隐藏可以通过名为 tabBar 的类属性访问的本机 Tab Bar。

其次,我们将 translatesAutoresizingMaskIntoConstraints 设置为 false,并向自定义 TabBar 组件添加一些阴影。

最后,我们必须将 Tab Bar ControllerselectedIndex 属性设置为起始值(最常见的情况是将该值设置为 0),并设置将由 Tab Bar Controller 处理的视图控制器。

由于我们的 Tab Bar Item 实现具有与每个枚举映射到其各自值的 View Controller 相关联的属性,而且它是 CaseIterable,因此我们可以轻松地将我们的枚举的所有情况映射到它们各自的 View Controller 值。映射后,它们将使用:

setViewControllers(_ controllers: [UIViewController], animated: true)

现在只剩下一步,我们必须处理当前 selectedIndex 的更改。

如果你在之前有所关注,那么你将立即知道如何实现这一点。当然是通过 CustomTabBar 类中以前创建的 Observable 序列,从而实现这个功能。

对于从 CustomTabBar 类外部发出的每个索引值,我们选择特定的选项卡,通过 selectTabWith(index: Int) 方法将作为参数传递的索引分配给Tab Bar Controller 的 selectedIndex 属性。

自定义Tab Bar实现的最终结果

至此,我们就完成了自定义Tab Bar的实现!

总结

总的来说,这篇文章详细介绍了如何创建一个自定义的 Tab Bar,为移动应用的 UI 设计增添了美感和交互性。我们从枚举类型的优势开始,解释了为什么使用枚举来定义 Tab Bar 的各个项目。通过对 Tab Bar 项的视图层的设计和动画效果,我们使 Tab Bar 更加生动和吸引人。

文章还深入讨论了自定义 Tab Bar 的实现,包括处理用户交互、布局和在 Tab Bar Controller 中的配置。我们使用了 RxSwift 等工具来处理项目选择,并展示了如何将所有这些部分组合在一起,以实现一个完全自定义的 Tab Bar。

最终,这个自定义 Tab Bar 为 UI 设计师和开发人员提供了一个更灵活的选择,使得移动应用的界面更加吸引人。希望本文的内容对于那些希望提升用户体验的开发人员和设计师来说是有益的,能够帮助他们更好地实现他们的创意和设计理念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/221111.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Postman和Apifox针对不同环境、全局变量的使用与比较

文章目录 一、Postman1、配置环境和全局变量2、验证3、存在问题分析 二、Apifox1、配置环境和全局参数2、创建公共脚本3、测试 总结 一、Postman 1、配置环境和全局变量 在Postman的界面中&#xff0c;点击"Environment"&#xff0c;添加我们需要的环境&#xff0c…

QML之动画的使用(含源码+注释)

文章目录 一、动画效果示例图二、个人理解三、源码总结 一、动画效果示例图 下图演示四组动画效果分别包含数值动画&#xff08;单个方块、多个方块&#xff09;&#xff0c;顺序动画&#xff0c;并行动画等效果 二、个人理解 NumberAnimation&#xff1a;改变控件属性值的…

力扣题:字符的统计-12.4

力扣题-12.4 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;657. 机器人能否返回原点 解题思想&#xff1a;进行统计即可 class Solution(object):def judgeCircle(self, moves):""":type moves: str:rtype: bool""&qu…

解决vue ssr服务端渲染运行时报错:net::ERR_PROXY_CONNECTION_FAILED

现象&#xff1a; 从代码里找了半天也没有找到问题&#xff0c;但是由于ssr服务端渲染配置本身非常复杂&#xff0c;步骤又繁琐&#xff0c; 而且报错又很多&#xff0c;不知道哪里出了问题。 感觉是header或者cookie丢失造成的&#xff0c;因为据说ssr本身有这样的缺陷&…

深信服行为管理AC设置用户定时注销

PS&#xff1a;设置用户无流量注销及每天定时注销 AC版本&#xff1a;AC13.0.62.001 Build20221107 官方通告&#xff1a; 截止标准版本AC12.0.80和AC13.0.80&#xff0c;暂不支持指定周期时间内注销一次所有用户&#xff0c;仅支持每天的固定时间注销所有用户&#xff0c;每…

『亚马逊云科技产品测评』活动征文|基于亚马逊EC2云服务器安装Bolo开源博客

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 亚马逊EC2云服务器&#xff08;Elastic Compute Cloud&#xff09;是亚马…

通过keepalived+nginx实现 k8s apiserver节点高可用

一、环境准备 K8s 主机配置&#xff1a; 配置&#xff1a; 4Gib 内存/4vCPU/60G 硬盘 网络&#xff1a;机器相互可以通信 k8s 实验环境网络规划&#xff1a; podSubnet&#xff08;pod 网段&#xff09; 10.244.0.0/16 serviceSubnet&#xff08;service 网段&#xff09;: 1…

nodejs+vue+ElementUi酒店餐饮客房点餐管理系统

系统非功能需求&#xff0c;只能是为了满足客户需求之外的非功能性要求。系统需要具有数据完整性验证的功能&#xff0c;对界面上非法的数据和不完整的数据进行提示&#xff0c;不能直接保存到数据库中&#xff0c;造成不完整性因素。运行软件:vscode 前端nodejsvueElementUi 语…

Linux 基础知识整理(三)

Linux文件和目录 Linux系统是一种典型的多用户系统&#xff0c;不同的用户有不一样的地位和权限。为了保护系统的安全性&#xff0c;Linux系统对不同的用户访问同一文件&#xff08;包括目录文件&#xff09;的权限做了不同的规定。 root权限最高&#xff0c;可以通过ls -l 或…

【踩坑记录】apex包的安装

你是不是也遇到了ModuleNotFoundError: No module named ‘packaging‘ error: subprocess-exited-with-error错误&#xff1f; apex介绍 此存储库包含nvidia维护的实用程序&#xff0c;以简化Pytorch中的混合精度和分布式训练。 这里的一些代码最终将包含在上游Pytorch中。 A…

vue3里面生命周期的使用

前言&#xff1a; vue2里面的生命周期和vue3生命周期是非常的相似的&#xff0c;我们通过访问生命周期钩子来处理不同场景之间的应用。 生命周期钩子的函数定义&#xff1a;每一个Vue组件实例在创建时都需要经历一系列的初始化步骤&#xff0c;比如数据侦听&#xff0c;编译模…

算法学习系列(五):N皇后、数独

目录 引言一、N皇后问题代码实现测试 二、数独问题代码实现测试 引言 这个N皇后问题是很典型的一个递归问题&#xff0c;就是还是要掌握&#xff0c;所谓递归其实就是dfs&#xff0c;一层一层深入下去。数独和N皇后的思路是一样的&#xff0c;只不过一些细节不同而已。 一、N…

使用pyscenedetect进行视频场景切割

1. 简介 在视频剪辑有转场一词&#xff1a;一个视频场景转换到另一个视频场景&#xff0c;场景与场景之间的过渡或转换&#xff0c;就叫做转场。 本篇介绍一个强大的开源工具PySceneDetect&#xff0c;它是一款基于opencv的视频场景切换检测和分析工具&#xff0c;项目地址: h…

探秘 Sass 之路:掌握强大的 CSS 预处理器(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

uni-app 微信小程序之swiper轮播图

1. 实现效果 2. 完成代码 <template><view class"components-home"><view style"margin-top:-50rpx;height: 486rpx; position: relative;margin-bottom: 80rpx;"><image srchttps://xxx.com/img/wccQQP.png modewidthFix classpng …

AI助力智慧农业,基于YOLOv3开发构建农田场景下的庄稼作物、田间杂草智能检测识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义&#xff0c;在前面的系列博文中&#xff0c;我们从一些现实世界里面的所见所想所感进行了很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《自建数据集&#xff0c;基于YOLOv7开发构建农田场景下杂草…

Ubuntu系统下使用apt-get安装Redis

记录一下Ubuntu20.04 64位系统下使用apt-get安装Redis 首先检查一下系统是否安装过redis whereis redismywmyw-K84HR:~$ whereis redis redis: mywmyw-K84HR:~$ 更新软件包 sudo apt-get update -y安装redis sudo apt-get install redis-server -ymywmyw-K84HR:~$ sudo apt…

ELasticsearch:什么是语义搜索?

语义搜索定义 语义搜索是一种解释单词和短语含义的搜索引擎技术。 语义搜索的结果将返回与查询含义匹配的内容&#xff0c;而不是与查询中的单词字面匹配的内容。 语义搜索是一组搜索引擎功能&#xff0c;其中包括根据搜索者的意图及其搜索上下文理解单词。 此类搜索旨在通过…

【S32K3环境搭建】-0.1-安装S32 Design Studio for S32 Platform 3.5

目录&#xff08;S32DS安装步骤详细&#xff09; 1 安装S32 Design Studio for S32 Platform 3.5准备工作 2 下载S32 Design Studio for S32 Platform 3.5安装包 2.1 获取S32DS的License许可 3 安装S32 Design Studio for S32 Platform 3.5 4 打开S32 Design Studio for S…

【网络奇缘】- 如何自己动手做一个五类|以太网|RJ45|网络电缆

​ ​ &#x1f308;个人主页: Aileen_0v0&#x1f525;系列专栏: 一见倾心,再见倾城 --- 计算机网络~&#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 本篇文章关于计算机网络的动手小实验---如何自己动手做一个网线&#xff0c; 也是为后面的物理层学习进…