关于自定义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的属性的情况,那么会发生一些意外的问题.什么问题呢,我们看图:

  

Snip20160719_9.png

Snip20160719_11.png

问题提出

有没有发现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,就算你传进去的是一个空的字典,依然会造成此现象.

Snip20160719_12.png

探究

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(@"==============================================");
}

以下是打印结果:

Snip20160719_13.png

为了方便说明,在截图中区分了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设定时的一致时不会出现此种情况的原因呢,既然有深拷贝,是不是对应的应该有浅拷贝?我们看下图就知道了.

Snip20160719_15.png

由图中可以看出,当修改的属性内容与控制器设定的一样(即: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.如下图所示.

Snip20160719_17.png

最后,如果有多个tabBarItem的属性被修改,那么修改的先后顺序也是tabBarController控制器中设定子控制器时的顺序.
以上均属个人推测,系统内部做了什么只有苹果官方知道,如有错误还望指正.

code: @XiYue on git.oschina.net.

关于自定义tabBar时修改系统自带tabBarItem属性造成的按钮顺序错乱的问题相关探究的更多相关文章

  1. iOS开发——运行时OC篇&使用运行时获取系统的属性:使用自己的手势修改系统自带的手势

    使用运行时获取系统的属性:使用自己的手势修改系统自带的手势 有的时候我需要实现一个功能,但是没有想到很好的方法或者想到了方法只是那个方法实现起来太麻烦,一或者确实为了装逼,我们就会想到iOS开发中最牛 ...

  2. Vue微信自定义分享时安卓系统config:ok,ios系统config:invalid signature签名错误,或者安卓和ios二次分享时均config:ok但是分享无效的解决办法

    简述需求:要求指定页面可以进行微信自定义分享(自定义标题,描述,图片,链接),剩下的页面隐藏所有基础接口.二次分享依然可以正常使用,切换至其他页面也可以正常进行自定义分享. 这两天在做微信自定义分享的 ...

  3. Activity设置全屏显示的两种方式及系统自带theme属性解析

    转载说明:原贴地址:http://blog.csdn.net/a_running_wolf/article/details/50480386 设置Activity隐藏标题栏.设置Activity全屏显 ...

  4. C#之系统自带保存属性

    源代码下载链接 程序开发很多时候需要根据运行环境做不通的参数配置,通过写ini之类的文本文件是一种方法,但这种方法也同时会把数据暴露 Winform开发中可以将需要配置的字段属性保存到程序中(其实也是 ...

  5. app整体搭建环境:tabBar切换不同控制器的封装(自定义导航+自定义uiviewcontroler+系统自带tabbar+自定义tabbarController)

    首先,一个app的搭建环境非常重要.既要实现基本功能,又要考虑后期优化的性能. 现在很多应用不仅仅是系统自带的控制器,由于需求复杂,基本上需要自定义多控制器来管理. 新建一个BasicNavigati ...

  6. iOS-tabBar切换不同控制器封装(自定义导航+自定义uiviewcontroler+系统自带tabbar+自定义tabbarController)

    首先,一个app的搭建环境非常重要.既要实现基本功能,又要考虑后期优化的性能. 现在很多应用不仅仅是系统自带的控制器,由于需求复杂,基本上需要自定义多控制器来管理. 新建一个BasicNavigati ...

  7. 配置Info.plist (设置状态栏样式、自定义定位时系统弹出的提示语、配置3DTouch应用快捷菜单)

    一.概述 iOS中很多功能需要配置Info.plist才能实现,如设置后台运行.支持打开的文件类型.自定义访问隐私内容时弹出的提示等.了解Info.plist中各字段及其含义,可以访问苹果开发网站相关 ...

  8. 使用storyboard显示UITableView时,如果不修改系统默认生成的tableView:cellForRowAtIndexPath:方法中的代码,则必须为UITableViewCell注册(填写)重用标识符:identifier.必须要代码方法中的标识符一致.

    CHENYILONG Blog 使用storyboard显示UITableView时,如果不修改系统默认生成的tableView:cellForRowAtIndexPath:方法中的代码,则必须为UI ...

  9. Android系统移植与调试之------->如何修改Android自带的apk出现一圈圈类似鸡蛋的花纹

    最近被一个问题烦恼到了,就是android4.1系统自带的Email.文件管理器.信息等apk都出现同一个问题,就是现实在平板上的时候会出现一圈圈类似鸡蛋的花纹. 我想了两种方法来解决,第一种方法没有 ...

随机推荐

  1. 【ArcGIS 10.2新特性】Portal for ArcGIS新特性

    1.概述 经过各版本的积累和更新,Portal for ArcGIS在ArcGIS10.2中以正式产品的形态加入到了ArcGIS系列产品线中.它有3个主要定位:协同管理平台.在线制图平台以及内容管理平 ...

  2. 关于OPenGL和OSG的矩阵 (转)

    关于OPenGL和OSG的矩阵 矩阵真的是一个很神奇的数学工具, 虽然单纯从数学上看, 它并没有什么特别的意义, 但一旦用到空间中的坐标变换,它就“一遇风云便成龙”, 大显神威了.简单的工具实现了复杂 ...

  3. Nmap 源代码学习四 软件简单使用

    软件安装环境是win7.使用Zenmap, nmap6.49BETA2 扫描主机port nmap -T4 -A -v 192.168.0.207 输出结果: 扫描整个子网 nmap 192.168. ...

  4. oc-27-@property的参数

    //01加强-10 @property .4前 ) @property + 手动实现 ) @property int age; + @synthesize age;//get和set方法的声明和实现都 ...

  5. 学习笔记之高质量C++/C编程指南

    高质量C++/C编程指南 http://man.lupaworld.com/content/develop/c&c++/c/c.htm 高质量C++/C编程指南(附录 C :C++/C 试题的 ...

  6. 下一个系列学习列表Spring.net+NHibernate+MVC

    开源框架完美组合之Spring.NET + NHibernate + ASP.NET MVC + jQuery + easyUI 中英文双语言小型企业网站Demo 刘冬.NET 2011-08-19 ...

  7. 数据连接命令join

    join主要用来将两个相关联的文件连接起来.两个文件相关联的意思是指这两个文件中有一些字段是关联的,例如两个文件的第1个字段都是学号,且每个学生的学号是唯一的.像这种具有唯一性关联的文件,就可以使用j ...

  8. $.getJSON()方法的 callback说明

    $.getJSON()方法跨域 去取得服务器的json对象的时候,url的后缀最后带一个"callback=?"的参数作为成功的回调函数:如: var url = "${ ...

  9. A标签使用javascript:伪协议

    一.前言 今天,遇到一个别人挖的坑,问题是这样的. 做了一个列表页,可以筛选数据,有很多筛条件.主要是有input复选框和<a>标签两种.如图: 其中房价的筛选条件使用<a>标 ...

  10. Data Structure 之 二叉树

          在计算机科学中,二叉树是每个节点最多有两个子树的树结构.通常子树被称作“左子树”(left subtree)和“右子树”(right subtree).二叉树常被用于实现二叉查找树和二叉堆 ...