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

本文探讨了iOS开发中自定义tabBar时修改系统自带tabBarItem属性导致按钮顺序错乱的问题。通过测试代码分析,发现该问题是由特定条件下系统对tabBarItem的深拷贝操作引起的。
 

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

测试代码:http://git.oschina.net/Xiyue/TabBarItem_TEST

简书地址:http://www.jianshu.com/users/f599d56f0592/latest_articles


序引

现在的主流框架中,在通常情况下,tabBar的属性一般都在tabBarController中全局设定好,且设定后一般就不会去改动.此外,现在绝大部分的App中,tabBar都会自定义,重写 layoutSubviews 方法以实现重新布局Item. 例如:

  
 1 - (void)layoutSubviews{
 2 [super layoutSubviews];
 3 
 4         CGFloat btnX = 0;
 5         CGFloat btnY = 0;
 6         CGFloat btnW = self.frame.size.width / 5;
 7         CGFloat btnH = self.frame.size.height;
 8 
 9         NSInteger index = 0;
10         // 遍历子控件
11         for (UIView *tabBarButton in self.subviews) {
12             if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
13                 if (index == 2) {
14                     index += 1;
15                 }
16 
17                 btnX = index * btnW;
18                 tabBarButton.frame = CGRectMake(btnX, btnY, btnW, btnH);
19 
20                 index++;
21             }
22         }
23 }

 

但是,在这种情况下,如果存在需要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是私有控件,我们没办法查看内部的属性及实现逻辑,只能从一些蛛丝马迹上探究端倪了:

 1 - (void)layoutSubviews{
 2     for (UIView *tabBarButton in self.subviews) {
 3         if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
 4             NSLog(@"%@",tabBarButton);
 5         }
 6     }
 7      NSLog(@"---------------------------------------------");
 8     [super layoutSubviews];
 9 
10         CGFloat btnX = 0;
11         CGFloat btnY = 0;
12 
13         CGFloat btnW = self.frame.size.width / 5;
14         CGFloat btnH = self.frame.size.height;
15         NSInteger index = 0;
16         // 遍历子控件
17         for (UIView *tabBarButton in self.subviews) {
18             if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
19                 NSLog(@"%@",tabBarButton);
20                 if (index == 2) {
21                     index += 1;
22                 }
23 
24                 btnX = index * btnW;
25 
26                 tabBarButton.frame = CGRectMake(btnX, btnY, btnW, btnH);
27 
28                 index++;
29             }
30         }
31     NSLog(@"----------------------------------------------");
32     for (UIView *tabBarButton in self.subviews) {
33         if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
34             NSLog(@"%@",tabBarButton);
35         }
36     }
37     NSLog(@"==============================================");
38 }

 

以下是打印结果:


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.

转载于:https://www.cnblogs.com/xiyueNote/p/5813060.html

<think> 我们正在讨论的是在自定义TabBar中使用wx.switchTab进行页面跳转的问题。根据引用内容,我们可以总结以下几点: 1. **wx.switchTab的作用**:用于跳转到tabBar页面,并关闭其他所有非tabBar页面(引用[1]和[2])。 2. **自定义TabBar中的跳转问题**:在自定义TabBar组件中,无法直接使用wx.navigateTo跳转到非tabBar页面,只能使用switchTab跳转到已定义的tabBar页面(引用[3])。 3. **自定义TabBar的实现**:自定义TabBar是通过在app.json中设置"custom": true,然后创建custom-tab-bar组件来实现的(引用[3])。 4. **在自定义TabBar中实现特殊跳转需求**:如果需要在自定义TabBar中点击某个按钮跳转到一个新页面(非tabBar页面),不能直接写在自定义TabBar组件内,而是要在每个tabBar页面中添加一个组件,覆盖在TabBar上(引用[3])。 5. **使用wx.switchTab后页面数据不更新的问题**:可以通过在success回调中获取当前页面实例并重新调用onLoad方法来强制刷新(引用[4])。 ### 针对用户的问题自定义TabBar中wx.switchTab的使用方法和相关说明 #### 1. **自定义TabBar中如何使用wx.switchTab?** 在自定义TabBar组件中,你可以像在普通页面中一样使用wx.switchTab。例如,当你点击某个Tab,你可以触发一个事件处理函数,在该函数中使用wx.switchTab跳转到对应的tabBar页面。 ```javascript // 在自定义TabBar组件中的某个方法 methods: { switchTab(event) { const url = event.currentTarget.dataset.url; wx.switchTab({ url: url, success: function() { console.log('跳转成功'); } }); } } ``` 注意:在自定义TabBar中,每个Tab都应该对应一个在app.json中定义的tabBar页面路径。 #### 2. **自定义TabBar中跳转的特殊情况** - **跳转到非tabBar页面**:如果你需要点击自定义TabBar上的某个按钮(比如中间的一个特殊按钮)跳转到一个非tabBar页面(例如发布页面),按照引用[3]的建议,不应该在自定义TabBar组件内直接使用wx.navigateTo,因为这样跳转后,自定义TabBar会消失(因为非tabBar页面不会显示TabBar)。因此,正确的做法是: 1. 在每一个tabBar页面(即底部导航栏页面)中都放置一个覆盖在TabBar上方的组件(通常是一个绝对定位的按钮)。 2. 点击这个按钮,在当前页面(tabBar页面)中使用wx.navigateTo跳转。 - **在自定义TabBar中只能使用switchTab跳转**:这是因为自定义TabBar组件本身并不是一个页面,它只是一个组件,存在于每个tabBar页面中。所以,当你在自定义TabBar中使用wx.switchTab,实际上是在当前tabBar页面的上下文中跳转到另一个tabBar页面。 #### 3. **使用wx.switchTab后页面数据不更新的问题** 引用[4]提供了一种解决方案:在switchTab的success回调中,获取当前页面的实例,并调用其onLoad方法强制刷新。 ```javascript wx.switchTab({ url: url, success: function(res) { // 获取当前页面实例 let page = getCurrentPages().pop(); if (page) { page.onLoad(); // 重新加载当前页面的数据 } } }); ``` 注意:这种方法可能会导致页面闪烁,因为会重新执行页面的生命周期。另一种常见的做法是在页面的onShow生命周期函数中加载数据,这样每次页面显示都会刷新数据。 #### 4. **自定义TabBar的配置** 在app.json中配置自定义TabBar: ```json { "tabBar": { "custom": true, "list": [{ "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/user/user", "text": "我的" }] } } ``` 然后,在项目根目录下创建`custom-tab-bar`组件,该组件将作为自定义TabBar的模板。 ### 总结 在自定义TabBar中使用wx.switchTab与在普通页面中使用没有区别,都是用于跳转到tabBar页面。但需要注意: 1. 自定义TabBar中无法直接跳转到非tabBar页面,需要借助每个tabBar页面中的覆盖组件来实现。 2. 跳转后可能需要手动刷新页面数据,可以在success回调中调用当前页面的onLoad方法,或者利用onShow生命周期。 ### § 相关问题 § 1. 如何在自定义TabBar中实现一个特殊按钮(如发布按钮)跳转到非tabBar页面? 2. 使用wx.switchTab跳转后,为什么目标页面的数据没有更新?有哪些解决方案? 3. 自定义TabBar组件与普通页面组件有哪些不同? 4.自定义TabBar中,如何获取当前选中的Tab项并高亮显示? 5. 如何解决自定义TabBar在iOS和Android上的样式适配问题? </think> ### 自定义 TabBar 中使用 `wx.switchTab` 的深度解析 #### 一、核心概念与限制 在自定义 TabBar 中(`app.json` 设置 `"custom": true`),`wx.switchTab` 仍然是跳转至 TabBar 页面的唯一官方方法,但存在特殊限制: 1. **跳转逻辑变化** - 自定义 TabBar 组件无法直接使用导航 API(如 `wx.navigateTo`) - 必须在 **页面逻辑** 中调用 `wx.switchTab`,通过事件触发跳转[^3] 2. **页面刷新机制** ```javascript // 在页面JS中调用(非自定义组件内) wx.switchTab({ url: '/pages/index/index', success() { const page = getCurrentPages().pop() if(page) page.onLoad() // 强制刷新页面数据[^4] } }) ``` - 需手动触发 `onLoad` 刷新页面数据(系统不会自动刷新) #### 二、完整实现流程 1. **配置自定义 TabBar** ```json // app.json { "tabBar": { "custom": true, "list": [ {"pagePath": "pages/home/home", "text": "首页"}, {"pagePath": "pages/profile/profile", "text": "我的"} ] } } ``` 2. **自定义组件事件处理** ```javascript // custom-tab-bar/index.js Component({ methods: { handleSwitch(e) { const url = e.currentTarget.dataset.url // 通过自定义事件传递跳转参数到页面 this.triggerEvent('tabchange', { url }) } } }) ``` 3. **页面接收跳转指令** ```javascript // pages/home/home.js Page({ onTabChange(e) { wx.switchTab({ url: e.detail.url, success: () => { const pages = getCurrentPages() pages[pages.length-1].onLoad() // 刷新目标页 } }) } }) ``` #### 三、跳转场景解决方案 | 场景 | 解决方案 | |------|----------| | 普通 Tab 切换 | 在页面中直接调用 `wx.switchTab` | | TabBar 按钮跳非 Tab 页 | 在页面中放置覆盖组件调用 `wx.navigateTo`[^3] | | 跨页面数据传递 | 使用全局变量或 Vuex-like 状态管理 | | 页面返回刷新 | 在 `onShow()` 生命周期中处理数据 | #### 四、常见问题解决 1. **页面不刷新问题** ```javascript // 目标页面添加监听 Page({ onShow() { if(typeof this.getTabBar === 'function') { this.getTabBar().setData({ active: 0 }) // 更新TabBar状态 } this.loadData() // 重新加载数据 } }) ``` 2. **跳转动画卡顿优化** ```javascript wx.nextTick(() => { wx.switchTab({...}) // 延迟执行避免渲染阻塞 }) ``` 3. **自定义组件交互封装** ```html <!-- 自定义TabBar模板 --> <view bindtap="handleSwitch" data-url="/pages/home/home"> <image src="{{active === 0 ? activeIcon : icon}}"></image> <text>首页</text> </view> ``` > **关键提示**:自定义 TabBar 本质是全局组件,每个 TabBar 页面都会渲染独立实例,需通过 `this.getTabBar()` 方法访问组件实例[^3]。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值