iOS 自定义Tab页

news2024/9/21 14:44:11

在iOS里面可以用UISegmentedControl控件来表示Tab页,但其样式难以修改,我们一般会自定义Tab页。

1. 自定义Tab页

在这里我们首先定义UKTabItemView用来显示其中的标签页。

// 标签页代理
@protocol UKTabItemViewDelegate <NSObject>

- (void)onTabItemViewSelected:(UKTabItemView *)tabItemView;

@end

@interface UKTabItemView : UIView

@property(nonatomic, weak) id<UKTabItemViewDelegate> delegate;

// 设置标签页标题
- (void)setText:(NSString *)text;
// 设置标签页状态
- (void)setSelected:(BOOL)selected;

@end

@interface UKTabItemView ()

@property(nonatomic, strong) UIButton *itemButton;
@property(nonatomic, strong) UIView *indicatorView;

@end

@implementation UKTabItemView

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupInitialUI];
    }
    return self;
}

- (void)setupInitialUI {
    [self addSubview:self.itemButton];
    [self.itemButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.top.bottom.equalTo(self);
    }];
    
    [self addSubview:self.indicatorView];
    [self.indicatorView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self);
        make.height.equalTo(@2);
        make.centerX.equalTo(self);
        make.width.equalTo(@60);
    }];
}

- (void)setText:(NSString *)text {
    [self.itemButton setTitle:text forState:UIControlStateNormal];
}

- (void)setSelected:(BOOL)selected {
    [self.itemButton setSelected:selected];
    self.indicatorView.hidden = !selected;

    if (selected) {
        [self.itemButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
    } else {
        [self.itemButton.titleLabel setFont:[UIFont systemFontOfSize:15]];
    }
}

- (UIButton *)itemButton {
    if (!_itemButton) {
        _itemButton = [[UIButton alloc] init];
        [_itemButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [_itemButton setTitleColor:[UIColor blueColor] forState:UIControlStateSelected];
        
        [_itemButton.titleLabel setFont:[UIFont systemFontOfSize:15]];
        
        [_itemButton addTarget:self action:@selector(onItemClick:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _itemButton;
}

- (void)onItemClick:(UIButton *)sender {
    if (self.delegate) {
        [self.delegate onTabItemViewSelected:self];
    }
}

- (UIView *)indicatorView {
    if (!_indicatorView) {
        _indicatorView = [[UIView alloc] init];
        
        _indicatorView.layer.backgroundColor = [UIColor blueColor].CGColor;
        _indicatorView.layer.cornerRadius = 1;
        _indicatorView.layer.masksToBounds = YES;
        _indicatorView.hidden = YES;
    }
    return _indicatorView;
}

@end

自定义UKTabView,包含若干个UKTabItemView,选中的选项卡字体和颜色会有变化,下面的提示也会变亮。

@protocol UKTabViewDelegate <NSObject>

- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position;

@end

@interface UKTabView : UIView

@property(nonatomic, weak) id<UKTabViewDelegate> delegate;

- (void)setItems:(NSArray<NSString *> *)items selection:(NSInteger)selection;
- (void)setSelection:(NSInteger)selection;

@end

@interface UKTabView() <UKTabItemViewDelegate>

@property(nonatomic, assign) NSInteger selection;
@property(nonatomic, strong) NSMutableArray<UKTabItemView *> *tabItemViews;

@end

@implementation UKTabView

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupInitialUI];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupInitialUI];
    }
    return self;
}

- (void)setupInitialUI {
   _selection = -1;
    self.tabItemViews = [[NSMutableArray alloc] init];
}

- (void)setItems:(NSArray<NSString *> *)items selection:(NSInteger)selection {
    [self.tabItemViews removeAllObjects];
    
    UKTabItemView *lastItemView = nil;
    for (NSString *item in items) {
        UKTabItemView *tabItemView = [[UKTabItemView alloc] init];
        [tabItemView setText:item];
        [self addSubview:tabItemView];
        
        // 所有的选项卡都等分排列
        [tabItemView mas_makeConstraints:^(MASConstraintMaker *make) {
            if (lastItemView) {
                make.left.equalTo(lastItemView.mas_right);
            } else {
                make.left.equalTo(self);
            }
            make.top.bottom.equalTo(self);
            make.width.equalTo(self).multipliedBy(1.0/item.length);
        }];
        
        lastItemView = tabItemView;
        
        [self internalAddTabItemView:tabItemView];
    }
    
    [self setSelection:selection];
}

- (void)internalAddTabItemView:(UKTabItemView *)itemView {
    // 添加itemView,并用tag记录位置
    itemView.tag = self.tabItemViews.count;
    [self.tabItemViews addObject:itemView];
    
    itemView.delegate = self;
}

- (void)setSelection:(NSInteger)selection {
    if (selection >= 0) {
        if (selection != self.selection) {
            if (self.selection >= 0) {
                [self.tabItemViews[self.selection] setSelected:NO];
            }
            
            _selection = selection;
            [self.tabItemViews[self.selection] setSelected:YES];
        }
    }
}

#pragma mark - UKTabItemViewDelegate -
- (void)onTabItemViewSelected:(UKTabItemView *)tabItemView {
    [self setSelection:tabItemView.tag];
    
    [self.delegate onTabViewSelected:self position:tabItemView.tag];
}

@end

UIViewController里面,我们定义一个UKTabView,并添加三个选项卡

UKTabView *tabView = [[UKTabView alloc] initWithFrame:CGRectMake(10, 100, 320, 50)]
[tabView setItems:@[@"选项1", @"选项2", @"选项3"] selection:0];

[self.view addSubview:self.tabView];

效果如下
在这里插入图片描述

2. 与UIScrollView的互动

一个Tab页往往下面会有互动的界面,比如说UIScrollViewUICollectionView等,这里我们以UIScrollView来举例说明。一般这里的互动有两种,一种是Tab选项卡被选中后UIScrollView跟着变化,另一种是UIScrollView滚动后Tab选项卡跟着变化。

我们先添加一个显示图片的UIScrollView

UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(10, 170, 320, 150)];
scrollView.contentSize = CGSizeMake(320*3, 150);
scrollView.pagingEnabled = YES;
scrollView.showsHorizontalScrollIndicator = NO;
        
scrollView.delegate = self;
[self.view addSubview: scrollView];

for (int index = 1; index <= 3; index++) {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(320 * (index - 1), 0, 320, 150)];
    imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"switcher%d", index]];
    [scrollView addSubview:imageView];
}

添加UKTabView的代理,监听每次Tab选项卡变化

#pragma mark - UKTabViewDelegate -
- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position {
    [self.scrollView setContentOffset:CGPointMake(320 * position, 0) animated:YES];
}

添加UIScrollView的代理,当UIScrollView的滚动结束时,修改Tab选项卡的状态

#pragma mark - UIScrollViewDelegate -
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    CGFloat width = scrollView.contentOffset.x;
    NSInteger page = width/320 + 0.5;

    [self.tabView setSelection:page];
}

效果如下
在这里插入图片描述

3. 动态添加Tab选项卡

动态添加UKTabItemView,我们需要修改前面所有UKTabItemView的间距

- (void)addItemView:(UKTabItemView *)itemView {
    NSInteger len = self.subviews.count;

    for (UIView *view in self.subviews) {
        [view mas_updateConstraints:^(MASConstraintMaker *make) {
            make.width.equalTo(self).multipliedBy(1.0/(len + 1));
        }];
    }

    [self addSubview:itemView];
    [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
        if (len == 0) {
            make.left.equalTo(self);
        } else {
            make.left.equalTo(self.subviews[len - 1].mas_right);
        }
        make.top.bottom.equalTo(self);
        make.width.equalTo(self).multipliedBy(1.0/(len + 1));
    }];

    [self internalAddTabItemView:itemView];
}

4. 提示栏

在上面的例子里面,提示栏都是包含在UITabItemView里面的,有时候我们可能需要提示栏有动态移动的效果,那么我们就把提示栏在UKTabView中定义。

// 设置提示栏的宽度、高度和颜色等
- (void)setIndicatorWidth:(NSInteger)width height:(NSInteger)height radius:(NSInteger)radius color:(UIColor *)color {
    self.indicatorWidth = width;
    self.indicatorHeight = height;
    self.indicatorRadius = radius;
    self.indicatorColor = color;

    if (width > 0) {
        self.indicatorLayer.fillColor = self.indicatorColor.CGColor;
        [self.layer addSublayer:self.indicatorLayer];
    } else {
        [self.indicatorLayer removeFromSuperlayer];
    }
}

// 修改当前选项卡后,重新绘制提示栏
- (void)setSelection:(NSInteger)selection {
    if (selection >= 0) {
        if (selection != self.selection) {
            if (self.selection >= 0) {
                [self.tabItemViews[self.selection] setSelected:NO];
            }

            _selection = selection;
            [self.tabItemViews[self.selection] setSelected:YES];
        }
        [self drawIndicatorView];
    }
}

// ratio为偏移度
- (void)setSelection:(NSInteger)selection offsetRatio:(CGFloat)ratio {
    if (selection >= 0) {
        self.offsetRatio = ratio;
        
        [self setSelection:selection];
    }
}

// 绘制提示栏,我们利用CALayer的隐式动画来给提示栏添加动态效果
// 每次添加选项卡后,提示栏宽度都会被清空
// 提示栏宽度不能超过选项卡本身宽度
- (void)drawIndicatorView {
    if (self.indicatorWidth > 0 && self.frame.size.width > 0 && self.tabItemViews.count > 0) {
        CGFloat itemWidth = self.frame.size.width*1.0/self.tabItemViews.count;
        BOOL initialized = self.indicatorActualWidth != 0;

        CGFloat startX = itemWidth * self.selection + itemWidth * self.offsetRatio;

        if (!initialized) {
            self.indicatorActualWidth = self.indicatorWidth;

            if (itemWidth <= self.indicatorWidth) {
                self.indicatorActualWidth = itemWidth;
            }
        }

        if (self.indicatorActualWidth < itemWidth) {
            startX += (itemWidth - self.indicatorActualWidth) / 2;
        }

        // 绘制选项卡
        if (!initialized) {
            UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.indicatorActualWidth, self.indicatorHeight) cornerRadius:self.indicatorRadius];
            self.indicatorLayer.path = path.CGPath;
        }
        
        // 如果有偏移量,去除CALayer隐式动画
        BOOL anim = self.offsetRatio == 0;        
        if (!anim) {
            [CATransaction begin];
            [CATransaction setDisableActions:true];
        }

        self.indicatorLayer.frame = CGRectMake(startX, self.frame.size.height - self.indicatorHeight, self.indicatorActualWidth, self.indicatorHeight);

        if (!anim) {
            [CATransaction commit];
        }
    }
}

我们在scrollViewWillBeginDragging方法 里面区分UIScrollView的滚动是由手势触发的还是代码触发的。在scrollViewDidScroll方法里面,如果是手势触发的移动,状态栏按照比例跟着移动。

#pragma mark - UIScrollViewDelegate -
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    self.dragging = YES;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.dragging) {
        CGFloat width = scrollView.contentOffset.x;
        NSInteger page = width/320 + 0.5;

        [self.tabView setSelection:page offsetRatio:(width/320 - page)];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    CGFloat width = scrollView.contentOffset.x;
    NSInteger page = width/320 + 0.5;

    [self.tabView setSelection:page];
    self.dragging = NO;
}

效果如下

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka源码分析之Producer数据发送流程(四)

概述 书接上回的producer发送流程&#xff0c;在准备工作完成后&#xff0c;kafka的producer借助Sender和KafkaClient两大组件完成了数据的发送。其底层封装了java的NIO的组件channle以及selector&#xff0c;对于NIO组件不太熟悉的同学可以自行查询相关文档。 下面我整理了k…

从0搭建Vue3组件库(二):Monorepo项目搭建

本篇文章是从0搭建Vue3组件库系列文章第二篇,本篇文章将带领大家使用pnpm搭建一个简单的Monorepo项目,并完成包的关联与测试 什么是 Monorepo 其实很简单,就是一个代码库里包含很多的项目,而这些项目虽然是相关联的,但是在逻辑上是独立的,可以由不同人或者团队来维护 为什么…

Scala之集合(1)

目录 ​​​​​​​集合介绍&#xff1a; 不可变集合继承图&#xff1a;​编辑 可变集合继承图 数组&#xff1a; 不可变数组&#xff1a; 样例代码&#xff1a; 遍历集合的方法&#xff1a; 1.for循环 2.迭代器 3.转换成List列表&#xff1a; 4.使用foreach()函数&a…

WebServer项目(二)->linux网络编程基础知识

WebServer项目->linux网络编程基础知识其中&#xff0c;遇到的错误总结1). read&#xff1a;Connection reset by peer2).什么叫连接被重置&#xff1f;1. socket 介绍2. 字节序从主机字节序到网络字节序的转换函数&#xff1a;htons、htonl&#xff1b; 从网络字节序到主机…

科创人·中建三局一公司尹奎:数字化变革能创造全新行业,其意义超越形式、范式创新

尹奎 中建三局一公司技术中心主任 教授级高级工程师&#xff0c;BIM领域资深专家&#xff0c;完成10余个基于BIM的相关研究课题&#xff0c;获省部级以上科技进步奖 13 项&#xff1b;公开出版专著3部&#xff0c;参与编写“十二五”国家重点图书出版规划项目《BIM应用施工》&a…

CCS5.5环境设置

CCS5.5环境设置 文件编码格式设置利用断点导入*.dat文件先用Python生成*.dat文件DSP代码&#xff08;sys/bios&#xff09; 步骤利用strip6x工具去除**.out文件中的调试信息硬件跟踪功能应用名词解释使用方法 ccs显示图片参数设置 文件编码格式设置 可分别对工作空间、工程、单…

Mysql下载安装

1.Mysql官网下载 MySQLhttps://www.mysql.com/ 有商业版和社区版&#xff0c;商业版使用收费&#xff0c;有试用期&#xff0c;社区版免费&#xff0c;选择社区版即可&#xff1a; 点击MySQL社区服务器&#xff1a; 选择要安装的版本&#xff1a; 进行下载即可&#xff1a; 2…

Docker设置http proxy代理

需求&#xff1a; 由于公司服务器无法正常访问公网&#xff0c;想要下载一些外部依赖包需要配置公司的内部代理。 Docker构建镜像或拉取镜像时需要通过代理访问外网&#xff0c;可以按照以下步骤设置HTTP代理 目录 创建目录 创建并编辑配置文件 重新加载Docker服务配置 重启…

Devops流程探究

1、DevOps面向对象 软件开发是由开发团队和运维团队共同协同配合才能完成一个软件的开发。 2、开发团队和运维团队 开发团队主要负责软件的开发和迭代更新&#xff0c;运维团队则是负责测试和部署上线。 3、解决问题 但是&#xff0c;这样会存在一个问题&#xff0c;只有当…

第五章 数据链路层与局域网

数据链路层服务 组帧&#xff1b;将要传输的数据封装成帧链路接入&#xff1b;可分为点对点链路&#xff08;独占&#xff09;和广播链路&#xff08;共享&#xff09;可靠交付&#xff1b;即在相邻节点间经数据链路实现数据报的可靠传输差错控制&#xff1b;检错纠错 差错控…

前端基础复习

1.什么叫HTML5&#xff1f;和原本的所说的HTML有什么区别&#xff1f; 本质上html和html5是一样的的。区别有&#xff1a; 1. 在文档类型声明上 HTML4.0 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loos…

密码加密之bcrypt

在这里是用的bcrypt加密算法&#xff0c;这种现在比较流行 而且无法进行解密 引入依赖 <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</ar…

JetBrains AppCode 2023.1 (macOS x64、aarch64) - 适用于 iOS/macOS 开发的智能 IDE

Xcode 14.3 compatibility, Swift refactorings and intentions, the IDE’s UI, and Kotlin Multiplatform Mobile. 请访问原文链接&#xff1a;https://sysin.org/blog/jb-appcode-2023/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a…

技术分享 | OMS 初识

作者&#xff1a;高鹏 DBA&#xff0c;负责项目日常问题排查&#xff0c;广告位长期出租 。 本文来源&#xff1a;原创投稿 *爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。 本文主要贡献者&#xff1a;进行OMS源码分析的…

GPT-4,大增长时代的序幕

虽然我们早在 2017 年就预测了超大模型的到来&#xff0c;因此才搞了分布式深度学习框架 OneFlow&#xff08;github.com/Oneflow-Inc/oneflow/&#xff09;&#xff0c;且 2020 年的 GPT-3 也掀起了大模型热潮&#xff08;OneFlow—— 让每一位算法工程师都有能力训练 GPT&…

BGP相关实验

实验要求及其拓扑图 划分好IP的拓扑图 实验分析 1,AS1存在两个环回&#xff0c;一个地址为192.168.1.0/24该地址不能在任何协议中宣告&#xff0c;AS3中存在两个环回&#xff0c;一个地址为192.168.2.0/24该地址不能在任何协议中宣告&#xff0c;最终要求这两个环回可以互相…

30天学会《Streamlit》(4)

30学会《Streamlit》是一项编码挑战&#xff0c;旨在帮助您开始构建Streamlit应用程序。特别是&#xff0c;您将能够&#xff1a; 为构建Streamlit应用程序设置编码环境 构建您的第一个Streamlit应用程序 了解用于Streamlit应用程序的所有很棒的输入/输出小部件 第4天 - st…

【NLP】自然语言处理_NLP入门——分词和词性标注

【NLP】自然语言处理_NLP入门——分词和词性标注 文章目录 【NLP】自然语言处理_NLP入门——分词和词性标注1. 介绍2. 概念和工具2.1 分词2.2 词性标注2.3 NLTK2.4 Jieba2.5 LAC 3. 代码实现举例3.1 分词3.1.1 使用nltk进行分词3.1.2 使用jieba进行分词3.1.3 使用LAC进行分词 3…

定义全局变量property与getprop

authordaisy.skye的博客_CSDN博客-Qt,嵌入式,Linux领域博主 adb调试 adb shell getprop .adb logcat 报错 init: sys_prop: permission denied uid:1006 name:ro.camera.gc02m1 在linux驱动中查找 find ./ -name *.c | xargs grep -n "property_set" find ./ -n…

《2023金融科技·校园招聘白皮书》新鲜出炉|牛客独家

数智创新时代&#xff0c;科技人才为先。 眼下&#xff0c;在建设“数字中国”的时代背景下&#xff0c;金融行业全面数智化转型已箭在弦上。政策端&#xff0c;金融行业为中共中央、国务院印发《数字中国建设整体布局规划》的7大重点行业之一。 资本端&#xff0c;仅2022年三…