关于自定义tabBar时修改系统自带tabBarItem属性造成的按钮顺序错乱的问题相关探究
关于自定义tabBar时修改系统自带tabBarItem属性造成的按钮顺序错乱的问题相关探究
测试代码:http://git.oschina.net/Xiyue/TabBarItem_TEST
简书地址:http://www.jianshu.com/users/f599d56f0592/latest_articles
序引
现在的主流框架中,在通常情况下,tabBar的属性一般都在tabBarController中全局设定好,且设定后一般就不会去改动.此外,现在绝大部分的App中,tabBar都会自定义,重写 layoutSubviews 方法以实现重新布局Item. 例如:
- (void)layoutSubviews{
[super layoutSubviews]; CGFloat btnX = ;
CGFloat btnY = ;
CGFloat btnW = self.frame.size.width / ;
CGFloat btnH = self.frame.size.height; NSInteger index = ;
// 遍历子控件
for (UIView *tabBarButton in self.subviews) {
if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
if (index == ) {
index += ;
} btnX = index * btnW;
tabBarButton.frame = CGRectMake(btnX, btnY, btnW, btnH); index++;
}
}
}
但是,在这种情况下,如果存在需要tabBarController的子控制器中修改tabBarItem的属性的情况,那么会发生一些意外的问题.什么问题呢,我们看图:


问题提出
有没有发现tabBarController中设置子控制器的顺序与运行显示的结果不一样?我们设置的第一个控制器莫名奇妙跑到最后一个去了,但是在程序启动后,默认显示在window上的依然是第一个 "我"这个控制器的view.也就是说: selectedViewController没有变,是默认tabBarController中设定子控制的顺序的第1个(childViewControllers[0]).但是该子控制器所绑定的tabBarItem所在的位置却发生了变化.
原因查找
什么原因引起的变化?测试发现,这个一个组合拳的效果:
- 条件 1:自定义tabBar并重写 layoutSubviews 方法 并且 自定义布局;如果没有重写layoutSubviews方法,也不会出现此问题;
- 条件 2:修改系统自带tabBarItem的属性,以下对常用属性举例:
- 2.1 title(tabBarItem.title)这个属性如果修改的title与tabBarController中设定的title一致,不会发生此现象;修改为不一样才能发生此现象.
- 2.2 image及selectedImage及TitleTextAttributes及TitleTextAttributes等涉及状态类的属性,不管与先前的属性是否相同,全部会发生此现象.特别是TitleTextAttributes,就算你传进去的是一个空的字典,依然会造成此现象.

探究
OK,既然重写 layoutSubviews 方法 并且 自定义布局 会发生此状况,而 重写但不自定义布局 却不会发生此状况,那么我们就从这里入手深入探究一下原因好了.
以下是我自己写的一些简单的输出Item的代码,因为UITabBarButton是私有控件,我们没办法查看内部的属性及实现逻辑,只能从一些蛛丝马迹上探究端倪了:
- (void)layoutSubviews{
for (UIView *tabBarButton in self.subviews) {
if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
NSLog(@"%@",tabBarButton);
}
}
NSLog(@"---------------------------------------------");
[super layoutSubviews]; CGFloat btnX = ;
CGFloat btnY = ; CGFloat btnW = self.frame.size.width / ;
CGFloat btnH = self.frame.size.height;
NSInteger index = ;
// 遍历子控件
for (UIView *tabBarButton in self.subviews) {
if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
NSLog(@"%@",tabBarButton);
if (index == ) {
index += ;
} btnX = index * btnW; tabBarButton.frame = CGRectMake(btnX, btnY, btnW, btnH); index++;
}
}
NSLog(@"----------------------------------------------");
for (UIView *tabBarButton in self.subviews) {
if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
NSLog(@"%@",tabBarButton);
}
}
NSLog(@"==============================================");
}
以下是打印结果:

为了方便说明,在截图中区分了ABCDEF六大区域,1-6留个标注frame变化点.
另外说明:
第一个等号(=)分割线之前的所有输出都是第一次来到 layoutSubviews 方法的打印结果;
第一个等号(=)分割线之后的所有输出都是修改tabBarItem属性后再次来到 layoutSubviews 方法的打印结果;
第一个减号(-)分割线前是[super layoutSubviews] 之前的打印结果;
第二个减号(-)分割线前是[super layoutSubviews] 之后,自定义布局前的的打印结果;
第二个减号(-)分割线后是自定义布局后的的打印结果.
- 首先 从A与B两个区域中,由标签1及标签2可以看出,系统默认的第一个UITabBarButton(系统的tabBarItem 类型为UITabBarButton类型)的位置坐标(origin)为(2,1),第一次自定义布局后变为(0,0),此时的这个UITabBarButton就是第一个子控制器('我')对应的tabBarItem,它的内存地址是:0x7fab39530010.(其他的内存地址也看一下,先有个印象,后面比较时会用上.layer层的内存地址也是一个比较依据.)
- 其次 再看C和D两个区域看出,从标签3 4 5看出:
- 修改了tabBarItem的属性后再次来到此方法时,已经找不到0x7fab39530010这个内存地址,而是多了一个0x7fab3961fc50内存地址,且是在tabBar.subviews数组的最后.layer层内存地址也是一样现象.
- 0x7fab39530010这个的frame是未进行第一次自定义布局前的frame.
- 观察其他tabBarItem的内存地址均未发生任何变化.layer层内存地址同样如此.
- 注意看红色箭头,不要被绿色标签6误导,它的内存地址显示它是原本tabBar.subviews中的第二个元素.
- 再次 从BD两个区域可以看出,第一次自定义布局完毕后与第二次自定义布局开始时的tabBar.subviews的frame已经不一样,但是内存地址上看却是,除去我们改变了属性的那个tabBarItem的内存地址不一样外,其他的全部一样.
猜想
鉴于tabBar为私有控件,无法查看内部的代码逻辑,再次对上述的一些显现进行猜想分析:
- A: tabBar内部会对属性进行set方法过滤,其中包括检查即将修改的属性与之前是否一致(除去state相关的,或者说state相关的都无法通过此过滤)
因此才会出现当改变title属性如果与tabBarController设定时的一致时不会出现此种情况的原因.逻辑内部如果通过了过滤,就执行某个处理,而这个处理就是造成这个现象的元凶- B>而这个元凶到底是什么呢?从前面的分析及截图中可以大概知道:虽然内存地址改变,但是指向的对象却是一个与先前属性完全相同的对象.这其实是 深拷贝 的套路对不对
那么为什么当改变title属性如果与tabBarController设定时的一致时不会出现此种情况的原因呢,既然有深拷贝,是不是对应的应该有浅拷贝?我们看下图就知道了.

由图中可以看出,当修改的属性内容与控制器设定的一样(即:self.title = @"我";)时,全程的内存地址都是一样的,没有发生任何变化,仅仅是frame中途发生了一些改变,变回了系统默认的.
那么:我们是否可以猜想:
1 : 事实上,每次layoutSubviews,系统内部的默认(注意 '默认' 这个关键字)做法是 浅拷贝 系统默认(childViewControllers顺序)的tabBarItem后重新计算frame,这是在[super layoutSubviews]中进行的;
2 :当对tabBarItem的一些属性进行修改时,就会执行set方法中的过滤;
(a)如果要修改成的属性与当前的完全一致(除去state相关的,或者说state相关的都无法通过此过滤)时,就是 浅拷贝 ,(也就是默认情况);
(b)当要修改成的属性与当前的完全不一致时,就是执行过滤后的逻辑,即 深拷贝;这就解释了为什么当修改某些属性时造成的原先的对象内存地址找不到了而是出现了另外一个新的内存地址,因为该tabBarItem指向的内存地址变成了指向深拷贝出来的那个对象的地址
- C : 至于为什么数组的顺序发生了改变呢,这个在我想过好多,以下是认为最大可能的一种想法:
未发生属性改变的tabBarItem浅拷贝一份地址后当做Subviews的基础数组,然后A深拷贝一份修改完数据后得到的新的数组A_new地址加到数组中,这样就排在了最后一个位置,但是childViewControllers的顺序没有改变,所以selectedViewController依然是A实例,因此发生程序启动后显示的是排在最后的tabBarItem所对应的控制器的view.如下图所示.

最后,如果有多个tabBarItem的属性被修改,那么修改的先后顺序也是tabBarController控制器中设定子控制器时的顺序.
以上均属个人推测,系统内部做了什么只有苹果官方知道,如有错误还望指正.
code: @XiYue on git.oschina.net.
关于自定义tabBar时修改系统自带tabBarItem属性造成的按钮顺序错乱的问题相关探究的更多相关文章
- iOS开发——运行时OC篇&使用运行时获取系统的属性:使用自己的手势修改系统自带的手势
使用运行时获取系统的属性:使用自己的手势修改系统自带的手势 有的时候我需要实现一个功能,但是没有想到很好的方法或者想到了方法只是那个方法实现起来太麻烦,一或者确实为了装逼,我们就会想到iOS开发中最牛 ...
- Vue微信自定义分享时安卓系统config:ok,ios系统config:invalid signature签名错误,或者安卓和ios二次分享时均config:ok但是分享无效的解决办法
简述需求:要求指定页面可以进行微信自定义分享(自定义标题,描述,图片,链接),剩下的页面隐藏所有基础接口.二次分享依然可以正常使用,切换至其他页面也可以正常进行自定义分享. 这两天在做微信自定义分享的 ...
- Activity设置全屏显示的两种方式及系统自带theme属性解析
转载说明:原贴地址:http://blog.csdn.net/a_running_wolf/article/details/50480386 设置Activity隐藏标题栏.设置Activity全屏显 ...
- C#之系统自带保存属性
源代码下载链接 程序开发很多时候需要根据运行环境做不通的参数配置,通过写ini之类的文本文件是一种方法,但这种方法也同时会把数据暴露 Winform开发中可以将需要配置的字段属性保存到程序中(其实也是 ...
- app整体搭建环境:tabBar切换不同控制器的封装(自定义导航+自定义uiviewcontroler+系统自带tabbar+自定义tabbarController)
首先,一个app的搭建环境非常重要.既要实现基本功能,又要考虑后期优化的性能. 现在很多应用不仅仅是系统自带的控制器,由于需求复杂,基本上需要自定义多控制器来管理. 新建一个BasicNavigati ...
- iOS-tabBar切换不同控制器封装(自定义导航+自定义uiviewcontroler+系统自带tabbar+自定义tabbarController)
首先,一个app的搭建环境非常重要.既要实现基本功能,又要考虑后期优化的性能. 现在很多应用不仅仅是系统自带的控制器,由于需求复杂,基本上需要自定义多控制器来管理. 新建一个BasicNavigati ...
- 配置Info.plist (设置状态栏样式、自定义定位时系统弹出的提示语、配置3DTouch应用快捷菜单)
一.概述 iOS中很多功能需要配置Info.plist才能实现,如设置后台运行.支持打开的文件类型.自定义访问隐私内容时弹出的提示等.了解Info.plist中各字段及其含义,可以访问苹果开发网站相关 ...
- 使用storyboard显示UITableView时,如果不修改系统默认生成的tableView:cellForRowAtIndexPath:方法中的代码,则必须为UITableViewCell注册(填写)重用标识符:identifier.必须要代码方法中的标识符一致.
CHENYILONG Blog 使用storyboard显示UITableView时,如果不修改系统默认生成的tableView:cellForRowAtIndexPath:方法中的代码,则必须为UI ...
- Android系统移植与调试之------->如何修改Android自带的apk出现一圈圈类似鸡蛋的花纹
最近被一个问题烦恼到了,就是android4.1系统自带的Email.文件管理器.信息等apk都出现同一个问题,就是现实在平板上的时候会出现一圈圈类似鸡蛋的花纹. 我想了两种方法来解决,第一种方法没有 ...
随机推荐
- hdu 1257 小希的迷宫 并查集
小希的迷宫 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=1272 D ...
- Codeforces Gym 100187E E. Two Labyrinths bfs
E. Two Labyrinths Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/gym/100187/prob ...
- SSI框架中配置log4j
事实上主要是log4j配置,跟SSI关系不大. web.xml中加入 <context-param> <param-name>log4jConfigLocation</p ...
- ASP.NET过滤HTML标签只保留换行与空格的方法
这篇文章主要介绍了ASP.NET过滤HTML标签只保留换行与空格的方法,包含网上常见的方法以及对此方法的改进,具有一定的参考借鉴价值,需要的朋友可以参考下 本文实例讲述了ASP.NET过滤HTML ...
- [Angular-Scaled Web] 7. Refactor code into Models
In the previous code, both categories and bookmarks are binded to $rootscope, or let says the same s ...
- iOS CocoaPods安装和使用图解
Cocoapods安装步骤 1.升级Ruby环境 sudo gem update --system 如果Ruby没有安装,请参考 如何在Mac OS X上安装 Ruby运行环境 2.安装CocoaPo ...
- IOS之以UIBezierPath绘制饼状图
1.绘制的饼状图是通过多个扇形拼和而成,绘制一个扇形也是比较简单的,核心代码如下: 先画一条圆弧,再画半径,接着再画一条圆弧,最后闭合路径: UIBezierPath* aPath = [[UIBe ...
- Java_Hbase优化
1.datanode的最大文件数 vi $HADOOP_HOME/conf/hdfs-site.xml <property> <name>dfs.datanode.max.xc ...
- 关于InputStream 和String对象之间的相互转换
代码如下: package com.xin.stream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; im ...
- PetaPoco使用要点
PetaPoco是一款适用于.Net 和Mono的微小.快速.单文件的微型ORM! 可以从这里获得PetaPoco: NuGet - http://nuget.org/List/Packages/Pe ...