关于自定义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.