关于自定义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. 【工作记录】c#操作win7注册表

    这里讲一 C# 小列子(高手请绕过此地! ), 我们平时都是在xp开发比较多...不过现在很多开发人员也在win7下开发了. 下面是在 LocalMachine 下的 一下注册表操作 ,就不详说了 p ...

  2. LuaFileSystem学习心得

    LuaFileSystem(简称lfs)是一个用于lua进行文件訪问的库,和Lua版本号同步.且是跨平台的,在为lua安装lfs之前须要先安装luarocks, luarocks是一个用于安装lua库 ...

  3. 专注网格剖分 - TetGen

    提要 记得大三那一年有一门课叫做高等有限元,最后的作业就是网格剖分算法的实现,我和同学一起花了些时间做了一个Qt程序,他写算法,我写界面,最后成绩竟然出奇的拿了90多... 今天要介绍的这款软件Tet ...

  4. oc-16-set,get方法

    S.h #import <Foundation/Foundation.h> /** 解决方案: 1.不用@public修饰 2.我们对象有访问和设置成员变量的两种操作 1>设置值 p ...

  5. 工作随笔记 点击除div自身之外的地方,关闭自己

    <div id="showSelectOptions" style="width:100px;height:100px;background-color:red;b ...

  6. Nginx的一些基本功能极速入门

    本文主要介绍一些Nginx的最基本功能以及简单配置,但不包括Nginx的安装部署以及实现原理. 1.静态HTTP服务器 首先,Nginx是一个HTTP服务器,可以将服务器上的静态文件(如HTML.图片 ...

  7. C#基础--.net平台的重要组成部分以及.net程序简单的编译原理

    .net平台的组成只要有两部分   FCL:框架类库    CLR:公共语言运行时 .net程序简单的编译原理 1.0:使用C#编译器(csc.exe) 将C#源代码编译成程序集+{编译之前:会检查C ...

  8. C++-copy constructor、copy-assignment operator、destructor

    本文由@呆代待殆原创,转载请注明出处. 对于一个类来说,我们把copy constructor.copy-assignment operator.move constructor.move-assig ...

  9. CSDN中根据文章自动生成文章目录

    概述 CSDN中有根据文件内容中H标签在文章中自动生成文章目录,看起来比较专业,就想把它搬到自己的博客园中.类似下图 提取JS脚本 通过浏览器开发者工具(IE/Chrome)找到产生文章目录javas ...

  10. leetcode 题解:Binary Tree Level Order Traversal (二叉树的层序遍历)

    题目: Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to ri ...