Closed MoLice closed 1 year ago
当 QMUITheme 切换时,会对当前所有 windows 的 rootViewController 调用他们的 qmui_themeDidChangeByManager:identifier:theme:
,而该方法的默认实现里会对所有的 childViewControllers
都触发相同的回调。
对于 UITabBarController
而言,它里面的 childViewControllers
是在 UITabBarController
init 时一次性加上去的,但只有切换到某个具体的 tab index 时,对应的 childViewController
的 view 才会被加载并添加到 window 上,如果这个 childViewController
是一个 UINavigationController
,那它的 UINavigationBar
也是在这时候才被添加到 window 上。
QMUI 配置表里对 UINavigationBar
的全局配置,内部是通过 UIAppearance
来实现的,而 UIAppearance
的特性就是在某个 UIView
实例被添加到 window 上时,它才会从 UIAppearance
里读取值,并自动设置到当前 UIView
实例里,但如果你在添加到 window 前就已经修改了这个实例的某些属性,则这些属性在后续应用 UIAppearance
的过程中不会被再次修改。
再者,系统在 iOS 13 时,创建了一套新的修改 UINavigationBar
样式的接口:UINavigationBarAppearance
,新旧两套接口是互斥关系,QMUI 为了让业务项目平滑兼容两套接口,在 UINavigationBar+QMUI
里做了 hook,把旧接口的调用都转为新接口。
所以在上图的代码里,在用旧接口修改 UINavigationBar
样式时,会触发①,①里会通过②转为新接口,在新接口的设置过程中,需要先通过③拿到当前 bar 的样式,修改后在④里赋值回去。
如果是正常情况,当前 bar 先被添加到 window 上,然后被 QMUI 全局配置的 UIAppearance
应用了正确的样式,后续再触发这些新旧接口的 hook 时,它在③里拿到的就是已经被 UIAppearance
修改后的值,后续的修改也是基于全局配置的基础上再修改,这样是没问题的。
但如果是本 issue 描述的场景,在 QMUITheme 切换时,尚未被添加到 window 上的 childViewController
也会收到回调,在回调里去修改 UINavigationBar
的样式,此时由于这些 UINavigationBar
还没应用 UIAppearance
,在步骤③里得到的其实是系统默认的值,修改后在④赋值回去,就会导致后续即便这些 UINavigationBar
被添加到 window 上了,系统也会认为你之前已经设置过自己的值(④),就不会再为你应用 UIAppearance
,所以你的 UINavigationBar
就会一直保持系统默认的样式不变,表现出来就是返回按钮与你全局配置的图片不一样。
如果你的项目使用 UIAppearance
来管理全局的 view 样式,则尽量避免在对应 view 的 didMoveToWindow
之前就去修改它的样式,所以这种场景请自行移除相关业务代码。
同时 QMUI 在 UINavigationBar(QMUI)
里会 hook setStandardAppearance:
,判断如果 setter 被触发时 bar.window
为空,则给出 QMUIAssert
提示。
也即 QMUI 仅对这种场景做提示,但不做任何兜底保护。
qmui_themeDidChangeByManager:identifier:theme:
的考虑该方法目前的实现如下:
如果我们只讨论 issue 这个场景,那么其实有一个更稳妥的修改方式,就是在 -[UIViewController qmui_themeDidChangeByManager:identifier:theme:]
默认实现里,针对 self 不同的类型做不同的处理,保证只对当前可视的 viewController 才会调用这个方法,这样业务在这个方法里不管怎么修改 appearance 都是安全的。类似的伪代码如下:
@implementation UIViewController (QMUITheme)
- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject<NSCopying> *)identifier theme:(__kindof NSObject *)theme {
if (presentedViewController) {
[presentedViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme];
return;
}
if ([self isKindOfClass:UITabBarController.class]) {
[selectedViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme];
return;
}
if ([self isKindOfClass:UINavigationController.class]) {
[topViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme];
return;
}
[self.childViewControllers enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull childViewController, NSUInteger idx, BOOL * _Nonnull stop) {
[childViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme];
}];
}
@end
但这样改会带来新的问题,如果只触发可视的 childViewController 的回调,则在 theme 切换后,从可视的 child 回到前一个 child,前面这个 child 无法感知到在它的生命周期内曾经有 theme 被切换过。假如这个 child 在内部有一些“记录当前是哪个 theme”的行为,则这些行为也会出错,并且唯一代替这个回调的方式就只有自己监听 QMUIThemeDidChangeNotification
,相对而言比较绕。
而 qmui_themeDidChangeByManager:identifier:theme:
设计的初衷就是希望为 UIViewController
提供一个便捷可靠的响应 theme 切换的方式——在这种情况下,冗余的调用比“猜测你可能需要响应的时机”更重要,因为猜测总是有不准确的时候。
综上,这里还是维持触发所有 childViewControllers 回调的做法,至于“这个回调可能比 viewWillAppear:、viewDidAppear: 都要早”的问题,暂时以注释的方式提醒,最终交给开发者自己处理。
已修复该问题,请根据 iOS 版本支持情况选择升级到 4.6.0(iOS 11-16) 或 4.6.1(iOS 13-16)。
Bug 表现 项目里为全局的返回按钮设置了一个图片:
某些情况下这个返回按钮的图片会恢复为系统默认的:
如何重现
UITabBarController
,里面添加多个 vc。qmui_themeDidChangeByManager:identifier:theme:
方法,在里面修改UINavigationBar
的样式,例如self.navigationController.navigationBar setBackgroundImage:xxx
。其他信息