Skip to content

Commit d136051

Browse files
author
molicechen
committed
4.6.0 - 兼容 iOS 16 及 iPhone 14
1 parent 802626e commit d136051

File tree

60 files changed

+1107
-327
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1107
-327
lines changed

QMUIConfigurationTemplate/QMUIConfigurationTemplate.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ - (void)applyConfigurationTemplate {
275275

276276
QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果
277277
QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向
278-
QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕
278+
QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕(仅 iOS 15 及以前版本需要,iOS 16 系统会自动处理,该开关无意义。)
279279
QMUICMI.defaultStatusBarStyle = UIStatusBarStyleDefault; // DefaultStatusBarStyle : 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 QMUIStatusBarStyleDarkContent。
280280
QMUICMI.needsBackBarButtonItemTitle = YES; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image
281281
QMUICMI.hidesBottomBarWhenPushedInitially = NO; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO

QMUIKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "QMUIKit"
3-
s.version = "4.5.1"
3+
s.version = "4.6.0"
44
s.summary = "致力于提高项目 UI 开发效率的解决方案"
55
s.description = <<-DESC
66
QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。

QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
#import "QMUIAsset.h"
1717
#import <Photos/Photos.h>
18-
#import <CoreServices/UTCoreTypes.h>
18+
#import <CoreServices/CoreServices.h>
1919
#import "QMUICore.h"
2020
#import "QMUIAssetsManager.h"
2121
#import "NSString+QMUI.h"

QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,11 @@ + (void)load {
140140
} else {
141141
// iOS 12 及以下系统,在不使用自定义 titleView 的情况下,在 viewWillAppear 时通过修改 navigationBar.titleTextAttributes 来设置新界面的导航栏标题样式,push 时是生效的,但 pop 时右边界面的样式会覆盖左边界面的样式,所以 pop 时的 titleTextAttributes 改为在 did pop 时处理
142142
// 如果用自定义 titleView 则没这种问题,只是为了代码简单,时机的选择不区分是否自定义 title
143-
[appearingViewController renderNavigationTitleStyleAnimated:animated];
143+
[appearingViewController renderNavigationBarTitleAppearanceAnimated:animated];
144144
[weakNavigationController qmui_animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
145145
// 这里要重新获取 topViewController,因为触发 pop 有两种:1. 普通完整的 pop;2.手势返回又取消。后者在 completion 里拿到的 topViewController 已经不是 completion 外面那个 appearingViewController 了,只有重新获取的 topViewController 才能代表最终可视的那个界面
146146
// https://github.com/Tencent/QMUI_iOS/issues/1210
147-
[weakNavigationController.topViewController renderNavigationTitleStyleAnimated:animated];
147+
[weakNavigationController.topViewController renderNavigationBarTitleAppearanceAnimated:animated];
148148
}];
149149
}
150150
}
@@ -215,6 +215,30 @@ + (void)load {
215215
}
216216
};
217217
});
218+
219+
if (@available(iOS 15.0, *)) {
220+
// - [UINavigationBar didMoveToWindow]
221+
OverrideImplementation([UINavigationBar class], @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
222+
return ^(UINavigationBar *selfObject) {
223+
224+
// call super
225+
void (*originSelectorIMP)(id, SEL);
226+
originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
227+
originSelectorIMP(selfObject, originCMD);
228+
229+
// 由于 renderNavigationBarStyleAnimated: 里对导航栏尚未添加到 window 上(UIAppearance 尚未被应用)的情况,跳过了 renderNavigationBarAppearanceAnimated:,所以这里在导航栏添加到 window 上时刷新一下导航栏样式
230+
// https://github.com/Tencent/QMUI_iOS/issues/1437
231+
if (selfObject.window) {
232+
UINavigationController *nav = (UINavigationController *)selfObject.qmui_viewController;
233+
if (![nav isKindOfClass:UINavigationController.class]) return;
234+
UIViewController *topViewController = nav.topViewController;
235+
if (topViewController.qmui_visibleState & QMUIViewControllerVisible) {// 加上这个 visibleState 的判断是因为一个普通的 UINavigationController 被初始化后导航栏默认就有一个 didMoveToWindow 的时机,这个时机里 topViewController 尚未触发 viewWillAppear:,如果不判断 visibleState,就会导致在过早的时候去设置导航栏样式,然后 viewWillAppear: 时又设置了一次。
236+
[topViewController renderNavigationBarAppearanceAnimated:NO];
237+
}
238+
}
239+
};
240+
});
241+
}
218242
});
219243
}
220244

@@ -267,7 +291,7 @@ - (void)layoutTransitionNavigationBar {
267291

268292
#pragma mark - 工具方法
269293

270-
// 根据当前的viewController,统一处理导航栏底部的分隔线、状态栏的颜色
294+
// 根据当前的viewController,统一处理导航栏的显隐、样式
271295
- (void)renderNavigationBarStyleAnimated:(BOOL)animated {
272296

273297
// 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
@@ -296,6 +320,33 @@ - (void)renderNavigationBarStyleAnimated:(BOOL)animated {
296320
}
297321
}
298322

323+
// 仅当导航栏被添加到 window 之后(UIAppearance 被应用之后),业务才可以设置导航栏的样式,否则在 UINavigationBar (QMUI) 里获取到的 navigationBar.standardAppearance 是系统默认的样式而不是 App 全局配置的样式,导致后续导航栏样式都是错的。
324+
// https://github.com/Tencent/QMUI_iOS/issues/1437
325+
if (@available(iOS 15.0, *)) {
326+
if (!navigationController.navigationBar.window) {
327+
return;
328+
}
329+
}
330+
331+
[self renderNavigationBarAppearanceAnimated:animated];
332+
}
333+
334+
// 仅处理导航栏的样式,不涉及显隐
335+
- (void)renderNavigationBarAppearanceAnimated:(BOOL)animated {
336+
337+
// 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
338+
if (![self.navigationController.viewControllers containsObject:self]) {
339+
return;
340+
}
341+
342+
if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) {
343+
return;
344+
}
345+
346+
// 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式
347+
UIViewController<QMUINavigationControllerAppearanceDelegate> *vc = (UIViewController<QMUINavigationControllerAppearanceDelegate> *)self;
348+
UINavigationController *navigationController = vc.navigationController;
349+
299350
// 导航栏的背景色
300351
if ([vc respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) {
301352
UIColor *barTintColor = [vc qmui_navigationBarBarTintColor];
@@ -361,11 +412,12 @@ - (void)renderNavigationBarStyleAnimated:(BOOL)animated {
361412
shouldRenderTitle = navigationController.qmui_navigationAction >= QMUINavigationActionUnknow && navigationController.qmui_navigationAction <= QMUINavigationActionPushCompleted;
362413
}
363414
if (shouldRenderTitle) {
364-
[vc renderNavigationTitleStyleAnimated:animated];
415+
[vc renderNavigationBarTitleAppearanceAnimated:animated];
365416
}
366417
}
367418

368-
- (void)renderNavigationTitleStyleAnimated:(BOOL)animated {
419+
// 仅处理导航栏标题
420+
- (void)renderNavigationBarTitleAppearanceAnimated:(BOOL)animated {
369421

370422
// 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
371423
if (![self.navigationController.viewControllers containsObject:self]) {

QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ - (void)viewDidLoad {
255255
[super viewDidLoad];
256256
self.view.backgroundColor = [UIColor clearColor];
257257
__weak __typeof(self)weakSelf = self;
258-
self.view.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) {
258+
self.view.qmui_hitTestBlock = ^__kindof UIView * _Nullable(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView) {
259259

260260
QMUIPopupMenuView *menuView = weakSelf.levelMenu.isShowing ? weakSelf.levelMenu : (weakSelf.nameMenu.isShowing ? weakSelf.nameMenu : nil);
261261
if (menuView && ![originalView isDescendantOfView:menuView]) {

QMUIKit/QMUIComponents/QMUIKeyboardManager.m

Lines changed: 66 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import "QMUILog.h"
1919
#import "QMUIAppearance.h"
2020
#import "QMUIMultipleDelegates.h"
21+
#import "NSArray+QMUI.h"
2122

2223
@class QMUIKeyboardViewFrameObserver;
2324
@protocol QMUIKeyboardViewFrameObserverDelegate <NSObject>
@@ -630,14 +631,10 @@ - (QMUIKeyboardUserInfo *)newUserInfoWithNotification:(NSNotification *)notifica
630631
}
631632

632633
- (BOOL)shouldReceiveShowNotification {
633-
634634
UIResponder *firstResponder = [self firstResponderInWindows];
635-
if (self.currentResponder) {
636-
// 这里有 BUG,如果点击了 webview 导致键盘下降,这个时候运行 shouldReceiveHideNotification 就会判断错误,所以如果发现是 nil 或是 WKContentView 则值不变
637-
if (firstResponder && ![firstResponder isKindOfClass:NSClassFromString([NSString stringWithFormat:@"%@%@", @"WK", @"ContentView"])]) {
638-
self.currentResponder = firstResponder;
639-
}
640-
} else {
635+
// 如果点击了 webview 导致键盘下降,这个时候运行 shouldReceiveHideNotification 就会判断错误,所以如果发现是 nil 或是 WKContentView 则值不变
636+
// WKContentView
637+
if (!self.currentResponder || (firstResponder && ![firstResponder isKindOfClass:NSClassFromString([NSString stringWithFormat:@"%@%@", @"WK", @"ContentView"])])) {
641638
self.currentResponder = firstResponder;
642639
}
643640

@@ -713,7 +710,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView {
713710
keyboardMoveUserInfo.animationOptions = self.lastUserInfo ? self.lastUserInfo.animationOptions : keyboardMoveUserInfo.animationCurve<<16;
714711
keyboardMoveUserInfo.beginFrame = self.keyboardMoveBeginRect;
715712
keyboardMoveUserInfo.endFrame = endFrame;
716-
keyboardMoveUserInfo.isFloatingKeyboard = CGRectGetWidth([self.class keyboardView].bounds) < CGRectGetWidth([UIScreen mainScreen].bounds);
713+
keyboardMoveUserInfo.isFloatingKeyboard = keyboardView ? CGRectGetWidth(keyboardView.bounds) < CGRectGetWidth(UIApplication.sharedApplication.delegate.window.bounds) : NO;
717714

718715
if (self.debug) {
719716
NSLog(@"keyboardDidMoveNotification - %@\n", self);
@@ -724,7 +721,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView {
724721
self.keyboardMoveBeginRect = endFrame;
725722

726723
if (self.currentResponder) {
727-
UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.windows.firstObject;
724+
UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window;
728725
if (mainWindow) {
729726
CGRect keyboardRect = keyboardMoveUserInfo.endFrame;
730727
CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect];
@@ -791,39 +788,13 @@ + (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardU
791788
}
792789
}
793790

794-
+ (UIWindow *)keyboardWindow {
795-
796-
for (UIWindow *window in UIApplication.sharedApplication.windows) {
797-
if ([self getKeyboardViewFromWindow:window]) {
798-
return window;
799-
}
800-
}
801-
802-
NSMutableArray *kbWindows = nil;
803-
804-
for (UIWindow *window in UIApplication.sharedApplication.windows) {
805-
NSString *windowName = NSStringFromClass(window.class);
806-
if ([windowName isEqualToString:[NSString stringWithFormat:@"UI%@%@", @"Remote", @"KeyboardWindow"]]) {
807-
// UIRemoteKeyboardWindow(iOS9 以下 UITextEffectsWindow)
808-
if (!kbWindows) kbWindows = [NSMutableArray new];
809-
[kbWindows addObject:window];
810-
}
811-
}
812-
813-
if (kbWindows.count == 1) {
814-
return kbWindows.firstObject;
815-
}
816-
817-
return nil;
818-
}
819-
820791
+ (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view {
821792

822793
if (CGRectIsNull(rect) || CGRectIsInfinite(rect)) {
823794
return rect;
824795
}
825796

826-
UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.windows.firstObject;
797+
UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window;
827798
if (!mainWindow) {
828799
if (view) {
829800
[view convertRect:rect fromView:nil];
@@ -861,39 +832,80 @@ + (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)re
861832
return distance;
862833
}
863834

835+
+ (UIWindow *)keyboardWindow {
836+
for (UIWindow *window in UIApplication.sharedApplication.windows) {
837+
if ([self positionedKeyboardViewInWindow:window]) {
838+
return window;
839+
}
840+
}
841+
842+
UIWindow *window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) {
843+
return [NSStringFromClass(item.class) isEqualToString:@"UIRemoteKeyboardWindow"];
844+
}];
845+
if (window) {
846+
return window;
847+
}
848+
849+
window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) {
850+
return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"];
851+
}];
852+
return window;
853+
}
854+
864855
+ (UIView *)keyboardView {
865856
for (UIWindow *window in UIApplication.sharedApplication.windows) {
866-
UIView *view = [self getKeyboardViewFromWindow:window];
857+
UIView *view = [self positionedKeyboardViewInWindow:window];
867858
if (view) {
868859
return view;
869860
}
870861
}
871862
return nil;
872863
}
873864

874-
+ (UIView *)getKeyboardViewFromWindow:(UIWindow *)window {
865+
/**
866+
从给定的 window 里寻找代表键盘当前布局位置的 view。
867+
iOS 15 及以前(包括用 Xcode 13 编译的 App 运行在 iOS 16 上的场景),键盘的 UI 层级是:
868+
|- UIApplication.windows
869+
|- UIRemoteKeyboardWindow
870+
|- UIInputSetContainerView
871+
|- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键)
872+
|- _UIKBCompatInputView - 键盘主体按键
873+
|- TUISystemInputAssistantView - 键盘顶部的候选词栏、emoji 键盘顶部的搜索框
874+
|- _UIRemoteKeyboardPlaceholderView - webView 里的输入工具栏的占位(实际的 view 在 UITextEffectsWindow 里)
875+
876+
iOS 16 及以后(仅限用 Xcode 14 及以上版本编译的 App),UIApplication.windows 里已经不存在 UIRemoteKeyboardWindow 了,所以退而求其次,我们通过 UITextEffectsWindow 里的 UIInputSetHostView 来获取键盘的位置——这两个 window 在布局层面可以理解为镜像关系。
877+
|- UIApplication.windows
878+
|- UITextEffectsWindow
879+
|- UIInputSetContainerView
880+
|- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键)
881+
|- _UIRemoteKeyboardPlaceholderView - 整个键盘区域,包含顶部候选词栏、emoji 键盘顶部搜索栏(有时候不一定存在)
882+
|- UIWebFormAccessory - webView 里的输入工具栏的占位
883+
|- TUIInputAssistantHostView - 外接键盘时可能存在,此时不一定有 placeholder
884+
|- UIInputSetHostView - 可能存在多个,但只有一个里面有 _UIRemoteKeyboardPlaceholderView
885+
886+
所以只要找到 UIInputSetHostView 即可,优先从 UIRemoteKeyboardWindow 找,不存在的话则从 UITextEffectsWindow 找。
887+
*/
888+
+ (UIView *)positionedKeyboardViewInWindow:(UIWindow *)window {
875889

876890
if (!window) return nil;
877891

878892
NSString *windowName = NSStringFromClass(window.class);
879-
if (![windowName isEqualToString:@"UIRemoteKeyboardWindow"]) {
880-
return nil;
893+
if ([windowName isEqualToString:@"UIRemoteKeyboardWindow"]) {
894+
UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
895+
return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetContainerView"];
896+
}].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
897+
return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetHostView"];
898+
}];
899+
return result;
881900
}
882-
883-
for (UIView *view in window.subviews) {
884-
NSString *viewName = NSStringFromClass(view.class);
885-
if (![viewName isEqualToString:@"UIInputSetContainerView"]) {
886-
continue;
887-
}
888-
for (UIView *subView in view.subviews) {
889-
NSString *subViewName = NSStringFromClass(subView.class);
890-
if (![subViewName isEqualToString:@"UIInputSetHostView"]) {
891-
continue;
892-
}
893-
return subView;
894-
}
901+
if ([windowName isEqualToString:@"UITextEffectsWindow"]) {
902+
UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
903+
return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetContainerView"];
904+
}].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
905+
return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetHostView"] && subview.subviews.count;
906+
}];
907+
return result;
895908
}
896-
897909
return nil;
898910
}
899911

QMUIKit/QMUIComponents/QMUINavigationTitleView.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ - (CGSize)accessorySpacingSize {
164164
- (CGSize)subAccessorySpacingSize {
165165
if (self.subAccessoryView) {
166166
UIView *view = self.subAccessoryView;
167-
return CGSizeMake(CGRectGetWidth(view.bounds) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.bounds));
167+
return CGSizeMake(CGRectGetWidth(view.frame) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.frame));
168168
}
169169
return CGSizeZero;
170170
}

0 commit comments

Comments
 (0)