diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m index 3e1c5ea7..3118ca56 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m @@ -18,7 +18,9 @@ #import "UINavigationBar+QMUI.h" #import "UINavigationBar+QMUIBarProtocol.h" #import "QMUIWeakObjectContainer.h" +#import "QMUIBarProtocolPrivate.h" #import "UIImage+QMUI.h" +#import "CALayer+QMUI.h" @implementation UINavigationBar (Transition) @@ -51,7 +53,7 @@ + (void)load { originSelectorIMP(selfObject, originCMD, appearance); if (selfObject.qmuinb_copyStylesToBar) { - selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance; + selfObject.qmuinb_copyStylesToBar.scrollEdgeAppearance = appearance; } }; }); @@ -222,6 +224,21 @@ + (void)load { }); } + OverrideImplementation(NSClassFromString(@"_UIBarBackground"), @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + _QMUITransitionNavigationBar *navigationBar = (_QMUITransitionNavigationBar *)selfObject.superview; + if (selfObject.window != nil && [navigationBar isKindOfClass:_QMUITransitionNavigationBar.class]) { + [navigationBar updateLayout]; + } + }; + }); + #ifdef IOS15_SDK_ALLOWED if (@available(iOS 15.0, *)) { // - [UINavigationBar _didMoveFromWindow:toWindow:] @@ -255,9 +272,12 @@ - (void)setOriginalNavigationBar:(UINavigationBar *)originBar { } - (void)layoutSubviews { - [super layoutSubviews]; - // 实测 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容 - self.qmui_backgroundView.frame = self.bounds; + [CALayer qmui_performWithoutAnimation:^{ + [super layoutSubviews]; + // 实测 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容 + self.qmui_backgroundView.frame = self.bounds; + [self.qmui_backgroundView layoutIfNeeded]; + }]; } // NavBarRemoveBackgroundEffectAutomatically 在开启了 AutomaticCustomNavigationBarTransitionStyle 时可能对假 bar 无效 @@ -274,7 +294,12 @@ - (void)updateLayout { [self.parentViewController.view bringSubviewToFront:self]; UIView *backgroundView = self.originalNavigationBar.qmui_backgroundView; CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.parentViewController.view]; - self.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112 + [CALayer qmui_performWithoutAnimation:^{ + // push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112 + // iOS 26上,y 可能是 -113 + self.frame = CGRectSetXY(rect, 0, 0); + [self layoutIfNeeded]; + }]; } } diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m index 21982331..308f263f 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m @@ -231,6 +231,8 @@ - (void)addTransitionNavigationBarAndBindNavigationBar:(BOOL)shouldBind { } _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init]; + /// iOS 26不设置items时子视图不会添加 + customBar.items = @[[[UINavigationItem alloc] initWithTitle:@""]]; customBar.parentViewController = self; self.transitionNavigationBar = customBar; diff --git a/QMUIKit/QMUIComponents/QMUIAlertController.h b/QMUIKit/QMUIComponents/QMUIAlertController.h index ca1a82be..44877437 100644 --- a/QMUIKit/QMUIComponents/QMUIAlertController.h +++ b/QMUIKit/QMUIComponents/QMUIAlertController.h @@ -241,6 +241,9 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { /// 显示`QMUIAlertController` - (void)showWithAnimated:(BOOL)animated; +/// 显示`QMUIAlertController` +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated; + /// 隐藏`QMUIAlertController` - (void)hideWithAnimated:(BOOL)animated; diff --git a/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUIKit/QMUIComponents/QMUIAlertController.m index 32a201a2..658b021f 100644 --- a/QMUIKit/QMUIComponents/QMUIAlertController.m +++ b/QMUIKit/QMUIComponents/QMUIAlertController.m @@ -891,6 +891,10 @@ - (void)customModalPresentationControllerAnimation { } - (void)showWithAnimated:(BOOL)animated { + [self showInWindow:nil animated:animated]; +} + +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated { if (self.willShow || self.showing) { return; } @@ -918,7 +922,7 @@ - (void)showWithAnimated:(BOOL)animated { __weak __typeof(self)weakSelf = self; - [self.modalPresentationViewController showWithAnimated:animated completion:^(BOOL finished) { + [self.modalPresentationViewController showInWindow:window animated:animated completion:^(BOOL finished) { weakSelf.dimmingView.alpha = 1; weakSelf.willShow = NO; weakSelf.showing = YES; diff --git a/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m b/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m index 9dbb0490..91629e83 100644 --- a/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m +++ b/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m @@ -87,6 +87,7 @@ - (void)setQmui_badgeString:(NSString *)qmui_badgeString { [self updateViewDidSetBlockIfNeeded]; } self.qmui_view.qmui_badgeString = qmui_badgeString; + self.qmui_selectedView.qmui_badgeString = qmui_badgeString; } - (NSString *)qmui_badgeString { @@ -97,6 +98,7 @@ - (NSString *)qmui_badgeString { - (void)setQmui_badgeBackgroundColor:(UIColor *)qmui_badgeBackgroundColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor, qmui_badgeBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeBackgroundColor = qmui_badgeBackgroundColor; + self.qmui_selectedView.qmui_badgeBackgroundColor = qmui_badgeBackgroundColor; } - (UIColor *)qmui_badgeBackgroundColor { @@ -107,6 +109,7 @@ - (UIColor *)qmui_badgeBackgroundColor { - (void)setQmui_badgeTextColor:(UIColor *)qmui_badgeTextColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor, qmui_badgeTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeTextColor = qmui_badgeTextColor; + self.qmui_selectedView.qmui_badgeTextColor = qmui_badgeTextColor; } - (UIColor *)qmui_badgeTextColor { @@ -117,6 +120,7 @@ - (UIColor *)qmui_badgeTextColor { - (void)setQmui_badgeFont:(UIFont *)qmui_badgeFont { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeFont, qmui_badgeFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeFont = qmui_badgeFont; + self.qmui_selectedView.qmui_badgeFont = qmui_badgeFont; } - (UIFont *)qmui_badgeFont { @@ -127,6 +131,7 @@ - (UIFont *)qmui_badgeFont { - (void)setQmui_badgeContentEdgeInsets:(UIEdgeInsets)qmui_badgeContentEdgeInsets { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets, [NSValue valueWithUIEdgeInsets:qmui_badgeContentEdgeInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeContentEdgeInsets = qmui_badgeContentEdgeInsets; + self.qmui_selectedView.qmui_badgeContentEdgeInsets = qmui_badgeContentEdgeInsets; } - (UIEdgeInsets)qmui_badgeContentEdgeInsets { @@ -137,6 +142,7 @@ - (UIEdgeInsets)qmui_badgeContentEdgeInsets { - (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeOffset = qmui_badgeOffset; + self.qmui_selectedView.qmui_badgeOffset = qmui_badgeOffset; } - (CGPoint)qmui_badgeOffset { @@ -147,6 +153,7 @@ - (CGPoint)qmui_badgeOffset { - (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_badgeOffsetLandscape = qmui_badgeOffsetLandscape; + self.qmui_selectedView.qmui_badgeOffsetLandscape = qmui_badgeOffsetLandscape; } - (CGPoint)qmui_badgeOffsetLandscape { @@ -155,6 +162,8 @@ - (CGPoint)qmui_badgeOffsetLandscape { - (void)setQmui_badgeView:(__kindof UIView *)qmui_badgeView { self.qmui_view.qmui_badgeView = qmui_badgeView; + /// iOS 26,需要改成block? + // self.qmui_selectedView.qmui_badgeView = qmui_badgeView; } - (__kindof UIView *)qmui_badgeView { @@ -163,6 +172,7 @@ - (__kindof UIView *)qmui_badgeView { - (void)setQmui_badgeViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock { self.qmui_view.qmui_badgeViewDidLayoutBlock = qmui_badgeViewDidLayoutBlock; + self.qmui_selectedView.qmui_badgeViewDidLayoutBlock = qmui_badgeViewDidLayoutBlock; } - (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock { @@ -178,6 +188,7 @@ - (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator [self updateViewDidSetBlockIfNeeded]; } self.qmui_view.qmui_shouldShowUpdatesIndicator = qmui_shouldShowUpdatesIndicator; + self.qmui_selectedView.qmui_shouldShowUpdatesIndicator = qmui_shouldShowUpdatesIndicator; } - (BOOL)qmui_shouldShowUpdatesIndicator { @@ -188,6 +199,7 @@ - (BOOL)qmui_shouldShowUpdatesIndicator { - (void)setQmui_updatesIndicatorColor:(UIColor *)qmui_updatesIndicatorColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor, qmui_updatesIndicatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_updatesIndicatorColor = qmui_updatesIndicatorColor; + self.qmui_selectedView.qmui_updatesIndicatorColor = qmui_updatesIndicatorColor; } - (UIColor *)qmui_updatesIndicatorColor { @@ -198,6 +210,7 @@ - (UIColor *)qmui_updatesIndicatorColor { - (void)setQmui_updatesIndicatorSize:(CGSize)qmui_updatesIndicatorSize { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize, [NSValue valueWithCGSize:qmui_updatesIndicatorSize], OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_updatesIndicatorSize = qmui_updatesIndicatorSize; + self.qmui_selectedView.qmui_updatesIndicatorSize = qmui_updatesIndicatorSize; } - (CGSize)qmui_updatesIndicatorSize { @@ -208,6 +221,7 @@ - (CGSize)qmui_updatesIndicatorSize { - (void)setQmui_updatesIndicatorOffset:(CGPoint)qmui_updatesIndicatorOffset { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset, @(qmui_updatesIndicatorOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_updatesIndicatorOffset = qmui_updatesIndicatorOffset; + self.qmui_selectedView.qmui_updatesIndicatorOffset = qmui_updatesIndicatorOffset; } - (CGPoint)qmui_updatesIndicatorOffset { @@ -218,6 +232,7 @@ - (CGPoint)qmui_updatesIndicatorOffset { - (void)setQmui_updatesIndicatorOffsetLandscape:(CGPoint)qmui_updatesIndicatorOffsetLandscape { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape, @(qmui_updatesIndicatorOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.qmui_view.qmui_updatesIndicatorOffsetLandscape = qmui_updatesIndicatorOffsetLandscape; + self.qmui_selectedView.qmui_updatesIndicatorOffsetLandscape = qmui_updatesIndicatorOffsetLandscape; } - (CGPoint)qmui_updatesIndicatorOffsetLandscape { @@ -226,6 +241,8 @@ - (CGPoint)qmui_updatesIndicatorOffsetLandscape { - (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView { self.qmui_view.qmui_updatesIndicatorView = qmui_updatesIndicatorView; + /// iOS 26,需要改成block? + // self.qmui_selectedView.qmui_updatesIndicatorView = qmui_updatesIndicatorView; } - (UIView *)qmui_updatesIndicatorView { @@ -234,6 +251,7 @@ - (UIView *)qmui_updatesIndicatorView { - (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock; + self.qmui_selectedView.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock; } - (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { @@ -242,23 +260,75 @@ - (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _ #pragma mark - Common +- (nullable UIView *)qmui_selectedView { + if (QMUIHelper.isUsedLiquidGlass) { + if (![self isKindOfClass:UITabBarItem.class]) { + return nil; + } + UIView *view = self.qmui_view; + if (!view) { + return nil; + } + NSInteger index = [view.superview.subviews indexOfObject:view]; + if (index == NSNotFound) { + return nil; + } + UIView *platterView = view.superview.superview; + if (![NSStringFromClass(platterView.class) hasSuffix:@"_UITabBarPlatterView"]) { + return nil; + } + UIView *selectedContentView = platterView.subviews.firstObject; + if (![NSStringFromClass(selectedContentView.class) hasSuffix:@"SelectedContentView"]) { + return nil; + } + if (index < selectedContentView.subviews.count) { + UIView *selectedView = [selectedContentView.subviews objectAtIndex:index]; + return selectedView; + } + } + return nil; +} + - (void)updateViewDidSetBlockIfNeeded { if (!self.qmui_viewDidSetBlock) { self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { + UIView *selectedView = item.qmui_selectedView; + view.qmui_badgeBackgroundColor = item.qmui_badgeBackgroundColor; + selectedView.qmui_badgeBackgroundColor = item.qmui_badgeBackgroundColor; + view.qmui_badgeTextColor = item.qmui_badgeTextColor; + selectedView.qmui_badgeTextColor = item.qmui_badgeTextColor; + view.qmui_badgeFont = item.qmui_badgeFont; + selectedView.qmui_badgeFont = item.qmui_badgeFont; + view.qmui_badgeContentEdgeInsets = item.qmui_badgeContentEdgeInsets; + selectedView.qmui_badgeContentEdgeInsets = item.qmui_badgeContentEdgeInsets; + view.qmui_badgeOffset = item.qmui_badgeOffset; + selectedView.qmui_badgeOffset = item.qmui_badgeOffset; + view.qmui_badgeOffsetLandscape = item.qmui_badgeOffsetLandscape; + selectedView.qmui_badgeOffsetLandscape = item.qmui_badgeOffsetLandscape; view.qmui_updatesIndicatorColor = item.qmui_updatesIndicatorColor; + selectedView.qmui_updatesIndicatorColor = item.qmui_updatesIndicatorColor; + view.qmui_updatesIndicatorSize = item.qmui_updatesIndicatorSize; + selectedView.qmui_updatesIndicatorSize = item.qmui_updatesIndicatorSize; + view.qmui_updatesIndicatorOffset = item.qmui_updatesIndicatorOffset; + selectedView.qmui_updatesIndicatorOffset = item.qmui_updatesIndicatorOffset; + view.qmui_updatesIndicatorOffsetLandscape = item.qmui_updatesIndicatorOffsetLandscape; + selectedView.qmui_updatesIndicatorOffsetLandscape = item.qmui_updatesIndicatorOffsetLandscape; view.qmui_badgeString = item.qmui_badgeString; + selectedView.qmui_badgeString = item.qmui_badgeString; + view.qmui_shouldShowUpdatesIndicator = item.qmui_shouldShowUpdatesIndicator; + selectedView.qmui_shouldShowUpdatesIndicator = item.qmui_shouldShowUpdatesIndicator; }; // 为 qmui_viewDidSetBlock 赋值前 item 已经 set 完 view,则手动触发一次 diff --git a/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m b/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m index 1bb929a5..e6a3744a 100644 --- a/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m +++ b/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m @@ -322,7 +322,7 @@ - (void)updateLayoutSubviewsBlockIfNeeded { // 不管 image 还是 text 的 UIBarButtonItem 都获取内部的 _UIModernBarButton 即可 - (UIView *)findBarButtonContentView { NSString *classString = NSStringFromClass(self.class); - if ([classString isEqualToString:@"UITabBarButton"]) { + if ([classString isEqualToString:@"UITabBarButton"] || [classString isEqualToString:@"_UITabButton"]) { // 特别的,对于 UITabBarItem,将 imageView 作为参考 view UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:self]; return imageView; diff --git a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m index b79bdebb..58cb16b5 100644 --- a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m +++ b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m @@ -99,7 +99,11 @@ - (void)renderButtonStyle { break; case QMUINavigationButtonTypeImage: // 拓展宽度,以保证用 leftBarButtonItems/rightBarButtonItems 时,按钮与按钮之间间距与系统的保持一致 - self.contentEdgeInsets = UIEdgeInsetsMake(0, 11, 0, 11); + if (QMUIHelper.isUsedLiquidGlass) { + self.contentEdgeInsets = UIEdgeInsetsZero; + } else { + self.contentEdgeInsets = UIEdgeInsetsMake(0, 11, 0, 11); + } break; case QMUINavigationButtonTypeBold: { font = NavBarButtonFontBold; @@ -122,14 +126,18 @@ - (void)renderButtonStyle { self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - // @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致 - UIOffset titleOffsetBaseOnSystem = UIOffsetMake(6, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整 - UIOffset configurationOffset = NavBarBarBackButtonTitlePositionAdjustment; - self.titleEdgeInsets = UIEdgeInsetsMake(titleOffsetBaseOnSystem.vertical + configurationOffset.vertical, titleOffsetBaseOnSystem.horizontal + configurationOffset.horizontal, -titleOffsetBaseOnSystem.vertical - configurationOffset.vertical, -titleOffsetBaseOnSystem.horizontal - configurationOffset.horizontal); - self.contentEdgeInsets = UIEdgeInsetsMake(0, - 0, - 0, - self.titleEdgeInsets.left); + if (QMUIHelper.isUsedLiquidGlass) { + self.contentEdgeInsets = UIEdgeInsetsZero; + } else { + // @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致 + UIOffset titleOffsetBaseOnSystem = UIOffsetMake(6, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整 + UIOffset configurationOffset = NavBarBarBackButtonTitlePositionAdjustment; + self.titleEdgeInsets = UIEdgeInsetsMake(titleOffsetBaseOnSystem.vertical + configurationOffset.vertical, titleOffsetBaseOnSystem.horizontal + configurationOffset.horizontal, -titleOffsetBaseOnSystem.vertical - configurationOffset.vertical, -titleOffsetBaseOnSystem.horizontal - configurationOffset.horizontal); + self.contentEdgeInsets = UIEdgeInsetsMake(0, + 0, + 0, + self.titleEdgeInsets.left); + } } break; @@ -207,29 +215,31 @@ - (void)tintColorDidChange { // 对按钮内容添加偏移,让UIBarButtonItem适配最新设备的系统行为,统一位置。注意 iOS 11 及以后,只有 image 类型的才会走进来 - (UIEdgeInsets)alignmentRectInsets { - - UIEdgeInsets insets = [super alignmentRectInsets]; - - if (self.type == QMUINavigationButtonTypeNormal || self.type == QMUINavigationButtonTypeBold) { - // 对于奇数大小的字号,不同 iOS 版本的偏移策略不同,统一一下 - if (self.titleLabel.font.pointSize / 2.0 > 0) { - insets.top = -PixelOne; - insets.bottom = PixelOne; - } - } else if (self.type == QMUINavigationButtonTypeImage) { - // 图片类型的按钮,分别对最左、最右那个按钮调整 inset(这里与 UINavigationItem(QMUINavigationButton) 里的 position 赋值配合使用) - if (self.buttonPosition == QMUINavigationButtonPositionLeft) { - insets.left = 11; - } else if (self.buttonPosition == QMUINavigationButtonPositionRight) { - insets.right = 11; + if (QMUIHelper.isUsedLiquidGlass) { + return [super alignmentRectInsets]; + } else { + UIEdgeInsets insets = [super alignmentRectInsets]; + if (self.type == QMUINavigationButtonTypeNormal || self.type == QMUINavigationButtonTypeBold) { + // 对于奇数大小的字号,不同 iOS 版本的偏移策略不同,统一一下 + if (self.titleLabel.font.pointSize / 2.0 > 0) { + insets.top = -PixelOne; + insets.bottom = PixelOne; + } + } else if (self.type == QMUINavigationButtonTypeImage) { + // 图片类型的按钮,分别对最左、最右那个按钮调整 inset(这里与 UINavigationItem(QMUINavigationButton) 里的 position 赋值配合使用) + if (self.buttonPosition == QMUINavigationButtonPositionLeft) { + insets.left = 11; + } else if (self.buttonPosition == QMUINavigationButtonPositionRight) { + insets.right = 11; + } + + insets.top = 1; + } else if (self.type == QMUINavigationButtonTypeBack) { + insets.top = PixelOne; } - insets.top = 1; - } else if (self.type == QMUINavigationButtonTypeBack) { - insets.top = PixelOne; + return insets; } - - return insets; } @end @@ -492,7 +502,7 @@ + (void)load { // result 有值意味着该事件本应属于 bar 的,这时候才干预。 // 属于 bar 但又分配给容器而不是精准的某个内容 view,此时才考虑扩大点击范围的识别。 - BOOL hitNothing = result == selfObject.qmui_contentView || [NSStringFromClass(result.class) containsString:@"StackView"]; + BOOL hitNothing = result == selfObject.qmui_contentView || ([NSStringFromClass(result.class) containsString:@"StackView"] && result.superview == selfObject.qmui_contentView); if (!hitNothing) return result; NSMutableArray *customViews = [[NSMutableArray alloc] init]; diff --git a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m index 5e7ed03f..fefb5eb4 100644 --- a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m +++ b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m @@ -20,6 +20,7 @@ #import "UIWindow+QMUI.h" #import "UIColor+QMUI.h" #import "QMUITextView.h" +#import "UIApplication+QMUI.h" /// 定义一个 class 只是为了在 Lookin 里表达这是一个 console window 而已,不需要实现什么东西 @interface QMUIConsoleWindow : UIWindow @@ -27,19 +28,30 @@ @interface QMUIConsoleWindow : UIWindow @implementation QMUIConsoleWindow -- (instancetype)init { - if (self = [super init]) { - self.backgroundColor = nil; - if (QMUICMIActivated) { - self.windowLevel = UIWindowLevelQMUIConsole; - } else { - self.windowLevel = 1; - } - self.qmui_capturesStatusBarAppearance = NO; +- (instancetype)initWithWindowScene:(UIWindowScene *)windowScene { + if (self = [super initWithWindowScene:windowScene]) { + [self didInitialize]; } return self; } +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.backgroundColor = nil; + if (QMUICMIActivated) { + self.windowLevel = UIWindowLevelQMUIConsole; + } else { + self.windowLevel = 1; + } + self.qmui_capturesStatusBarAppearance = NO; +} + - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // 当显示 QMUIConsole 时,点击空白区域,consoleViewController hitTest 会 return nil,从而将事件传递给 window,再由 window hitTest return nil 来把事件传递给 UIApplication.delegate.window。但在 iPad 12-inch 里,当 consoleViewController hitTest return nil 后,事件会错误地传递给 consoleViewController.view.superview(而不是 consoleWindow),不清楚原因,暂时做一下保护 // https://github.com/Tencent/QMUI_iOS/issues/1169 @@ -121,6 +133,7 @@ + (void)show { [console initConsoleWindowIfNeeded]; console.consoleWindow.alpha = 0; console.consoleWindow.hidden = NO; + console.consoleWindow.windowScene = UIApplication.sharedApplication.qmui_delegateWindow.windowScene; }]; [UIView animateWithDuration:.25 delay:.2 options:QMUIViewAnimationOptionsCurveOut animations:^{ console.consoleWindow.alpha = 1; @@ -130,11 +143,12 @@ + (void)show { + (void)hide { [QMUIConsole sharedInstance].consoleWindow.hidden = YES; + [QMUIConsole sharedInstance].consoleWindow.windowScene = nil; } - (void)initConsoleWindowIfNeeded { if (!self.consoleWindow) { - self.consoleWindow = [[QMUIConsoleWindow alloc] init]; + self.consoleWindow = [QMUIConsoleWindow qmui_windowWithWindowScene:UIApplication.sharedApplication.qmui_delegateWindow.windowScene]; self.consoleViewController = [[QMUIConsoleViewController alloc] init]; self.consoleWindow.rootViewController = self.consoleViewController; } diff --git a/QMUIKit/QMUIComponents/QMUIDialogViewController.h b/QMUIKit/QMUIComponents/QMUIDialogViewController.h index e1396ef7..e770a15d 100644 --- a/QMUIKit/QMUIComponents/QMUIDialogViewController.h +++ b/QMUIKit/QMUIComponents/QMUIDialogViewController.h @@ -127,6 +127,15 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)showWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; +/** + 显示弹窗 + + @param window 所在的window,默认为delegate.window + @param animated 是否以动画的形式显示 + @param completion 显示动画结束后的回调 + */ +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + /** 以动画形式隐藏弹窗,等同于 [self hideWithAnimated:YES completion:nil] */ diff --git a/QMUIKit/QMUIComponents/QMUIDialogViewController.m b/QMUIKit/QMUIComponents/QMUIDialogViewController.m index 94b2c1dc..0e5acef7 100644 --- a/QMUIKit/QMUIComponents/QMUIDialogViewController.m +++ b/QMUIKit/QMUIComponents/QMUIDialogViewController.m @@ -373,10 +373,14 @@ - (void)show { } - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + [self showInWindow:nil animated:animated completion:completion]; +} + +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion { self.modalPresentationViewController.contentViewMargins = self.dialogViewMargins; self.modalPresentationViewController.maximumContentViewWidth = self.maximumContentViewWidth; self.modalPresentationViewController.contentViewController = self; - [self.modalPresentationViewController showWithAnimated:YES completion:completion]; + [self.modalPresentationViewController showInWindow:window animated:animated completion:completion]; } - (void)hide { diff --git a/QMUIKit/QMUIComponents/QMUIKeyboardManager.m b/QMUIKit/QMUIComponents/QMUIKeyboardManager.m index 178df3ec..68cd5b2b 100644 --- a/QMUIKit/QMUIComponents/QMUIKeyboardManager.m +++ b/QMUIKit/QMUIComponents/QMUIKeyboardManager.m @@ -20,6 +20,7 @@ #import "QMUIMultipleDelegates.h" #import "NSArray+QMUI.h" #import "UIView+QMUI.h" +#import "UIApplication+QMUI.h" @class QMUIKeyboardViewFrameObserver; @protocol QMUIKeyboardViewFrameObserverDelegate @@ -396,10 +397,10 @@ - (UIResponder *)unPackageTargetResponder:(NSValue *)value { } - (UIResponder *)firstResponderInWindows { - UIResponder *responder = [UIApplication.sharedApplication.keyWindow qmui_findFirstResponder]; + UIResponder *responder = [UIApplication.sharedApplication.qmui_keyWindow qmui_findFirstResponder]; if (!responder) { - for (UIWindow *window in UIApplication.sharedApplication.windows) { - if (window != UIApplication.sharedApplication.keyWindow) { + for (UIWindow *window in UIApplication.sharedApplication.qmui_windows) { + if (window != UIApplication.sharedApplication.qmui_keyWindow) { responder = [window qmui_findFirstResponder]; if (responder) { return responder; @@ -709,7 +710,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView { keyboardMoveUserInfo.animationOptions = self.lastUserInfo ? self.lastUserInfo.animationOptions : keyboardMoveUserInfo.animationCurve<<16; keyboardMoveUserInfo.beginFrame = self.keyboardMoveBeginRect; keyboardMoveUserInfo.endFrame = endFrame; - keyboardMoveUserInfo.isFloatingKeyboard = keyboardView ? CGRectGetWidth(keyboardView.bounds) < CGRectGetWidth(UIApplication.sharedApplication.delegate.window.bounds) : NO; + keyboardMoveUserInfo.isFloatingKeyboard = keyboardView ? CGRectGetWidth(keyboardView.bounds) < CGRectGetWidth(UIApplication.sharedApplication.qmui_delegateWindow.bounds) : NO; if (self.debug) { NSLog(@"keyboardDidMoveNotification - %@\n", self); @@ -720,7 +721,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView { self.keyboardMoveBeginRect = endFrame; if (self.currentResponder) { - UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; + UIWindow *mainWindow = UIApplication.sharedApplication.qmui_keyWindow ?: UIApplication.sharedApplication.qmui_delegateWindow; if (mainWindow) { CGRect keyboardRect = keyboardMoveUserInfo.endFrame; CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect]; @@ -793,7 +794,7 @@ + (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view { return rect; } - UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; + UIWindow *mainWindow = UIApplication.sharedApplication.qmui_keyWindow ?: UIApplication.sharedApplication.qmui_delegateWindow; if (!mainWindow) { if (view) { [view convertRect:rect fromView:nil]; @@ -855,7 +856,7 @@ + (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)re 所以只要找到 UIInputSetHostView 即可,优先从 UIRemoteKeyboardWindow 找,不存在的话则从 UITextEffectsWindow 找。 */ + (UIView *)keyboardView { - UIView *inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { + UIView *inputSetHostView = [[UIApplication.sharedApplication.qmui_windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { return [NSStringFromClass(window.class) isEqualToString:@"UIRemoteKeyboardWindow"]; }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) { return [self inputSetHostViewInWindow:window]; @@ -863,7 +864,7 @@ + (UIView *)keyboardView { if (inputSetHostView) return inputSetHostView; - inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { + inputSetHostView = [[UIApplication.sharedApplication.qmui_windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { return [NSStringFromClass(window.class) isEqualToString:@"UITextEffectsWindow"]; }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) { return [self inputSetHostViewInWindow:window]; @@ -873,6 +874,16 @@ + (UIView *)keyboardView { } + (UIView *)inputSetHostViewInWindow:(UIWindow *)window { + if (QMUIHelper.isUsedLiquidGlass) { + UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { + return [NSStringFromClass(subview.class) isEqualToString:@"UITrackingWindowView"]; + }].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { + return [NSStringFromClass(subview.class) isEqualToString:@"UIKeyboardItemContainerView"] && subview.subviews.count; + }]; + if (result) { + return result; + } + } UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetContainerView"]; }].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { @@ -885,14 +896,14 @@ + (UIWindow *)keyboardWindow { UIView *inputSetHostView = [self keyboardView]; if (inputSetHostView) return inputSetHostView.window; - UIWindow *window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { + UIWindow *window = [UIApplication.sharedApplication.qmui_windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { return [NSStringFromClass(item.class) isEqualToString:@"UIRemoteKeyboardWindow"]; }]; if (window) { return window; } - window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { + window = [UIApplication.sharedApplication.qmui_windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"]; }]; return window; @@ -929,7 +940,7 @@ + (CGFloat)visibleKeyboardHeight { // iPad“侧拉”模式打开的 App,App Window 和键盘 Window 尺寸不同,如果以键盘 Window 为准则会认为键盘一直在屏幕上,从而出现误判,所以这里改为用 App Window。 // iPhone、iPad 全屏/分屏/台前调度,都没这个问题 // UIWindow *keyboardWindow = keyboardView.window; - UIWindow *keyboardWindow = UIApplication.sharedApplication.delegate.window; + UIWindow *keyboardWindow = UIApplication.sharedApplication.qmui_delegateWindow; if (!keyboardView || !keyboardWindow) { return 0; } else { diff --git a/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h b/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h index b44011fa..2ef65798 100644 --- a/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h +++ b/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h @@ -250,6 +250,14 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { */ - (void)showWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; +/** + * 将浮层以 UIWindow 的方式显示出来 + * @param window 所在的window,默认为delegate.window + * @param animated 是否以动画的形式显示 + * @param completion 显示动画结束后的回调 + */ +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + /** * 将浮层隐藏掉 * @param animated 是否以动画的形式隐藏 @@ -314,7 +322,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @end -/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.windows`里方便地区分出来 +/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.qmui_windows`里方便地区分出来 @interface QMUIModalPresentationWindow : UIWindow @end diff --git a/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m b/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m index 5355412f..06ab6a54 100644 --- a/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m +++ b/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m @@ -20,6 +20,7 @@ #import "QMUIKeyboardManager.h" #import "UIWindow+QMUI.h" #import "QMUIAppearance.h" +#import "UIApplication+QMUI.h" @interface UIViewController () @@ -75,6 +76,8 @@ @interface QMUIModalPresentationViewController () @property(nonatomic, strong) QMUIKeyboardManager *keyboardManager; @property(nonatomic, assign) CGFloat keyboardHeight; @property(nonatomic, assign) BOOL avoidKeyboardLayout; +@property(nonatomic, assign) CGFloat initializeKeyboardHeight; // 默认为-1 + @end @implementation QMUIModalPresentationViewController @@ -96,6 +99,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)didInitialize { [self qmui_applyAppearance]; + self.initializeKeyboardHeight = -1; self.shouldDimmedAppAutomatically = YES; self.onlyRespondsToKeyboardEventFromDescendantViews = YES; self.shouldBecomeKeyWindow = YES; @@ -140,6 +144,15 @@ - (void)viewDidLoad { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; + // 获取一次键盘高度,兼容键盘已经弹起时情况 + if (self.initializeKeyboardHeight == -1) { + CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:QMUIKeyboardManager.currentKeyboardFrame toView:self.view]; + CGRect visibleRect = CGRectIntersection(CGRectFlatted(self.view.bounds), CGRectFlatted(keyboardRect)); + if (CGRectIsValidated(visibleRect)) { + self.keyboardHeight = visibleRect.size.height; + } + self.initializeKeyboardHeight = self.keyboardHeight; + } self.dimmingView.frame = self.view.bounds; @@ -271,15 +284,16 @@ - (void)viewWillDisappear:(BOOL)animated { if (self.shownInWindowMode) { // 恢复 keyWindow 之前做一下检查,避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/90 - if (UIApplication.sharedApplication.keyWindow == self.window) { + if (UIApplication.sharedApplication.qmui_keyWindow == self.window) { if (self.previousKeyWindow.hidden) { // 保护了这个 issue 记录的情况,避免主 window 丢失 keyWindow https://github.com/Tencent/QMUI_iOS/issues/315 - [UIApplication.sharedApplication.delegate.window makeKeyWindow]; + [UIApplication.sharedApplication.qmui_delegateWindow makeKeyWindow]; } else { [self.previousKeyWindow makeKeyWindow]; } } self.window.hidden = YES; + self.window.windowScene = nil; self.window.rootViewController = nil; self.previousKeyWindow = nil; [self endAppearanceTransition]; @@ -499,15 +513,20 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { } - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + [self showInWindow:nil animated:animated completion:completion]; +} + +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion { if (self.visible) return; self.visible = YES; // makeKeyAndVisible 导致的 viewWillAppear: 必定 animated 是 NO 的,所以这里用额外的变量保存这个 animated 的值 self.appearAnimated = animated; self.appearCompletionBlock = completion; - self.previousKeyWindow = UIApplication.sharedApplication.keyWindow; + self.previousKeyWindow = UIApplication.sharedApplication.qmui_keyWindow; if (!self.window) { - self.window = [[QMUIModalPresentationWindow alloc] init]; + UIWindowScene *windowScene = window.windowScene ? : UIApplication.sharedApplication.qmui_delegateWindow.windowScene; + self.window = [QMUIModalPresentationWindow qmui_windowWithWindowScene:windowScene]; self.window.windowLevel = UIWindowLevelQMUIAlertView; self.window.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色 [self updateWindowStatusBarCapture]; @@ -672,12 +691,14 @@ - (BOOL)isShowingPresentedViewController { #pragma mark - - (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.onlyRespondsToKeyboardEventFromDescendantViews) { + if (self.onlyRespondsToKeyboardEventFromDescendantViews && self.initializeKeyboardHeight == 0) { UIResponder *firstResponder = keyboardUserInfo.targetResponder; if (!firstResponder || !([firstResponder isKindOfClass:[UIView class]] && [(UIView *)firstResponder isDescendantOfView:self.view])) { return; } } + self.initializeKeyboardHeight = 0; + CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view]; if (self.keyboardHeight != keyboardHeight) { self.keyboardHeight = keyboardHeight; @@ -751,7 +772,7 @@ - (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { @implementation QMUIModalPresentationViewController (Manager) + (BOOL)isAnyModalPresentationViewControllerVisible { - for (UIWindow *window in UIApplication.sharedApplication.windows) { + for (UIWindow *window in UIApplication.sharedApplication.qmui_windows) { if ([window isKindOfClass:[QMUIModalPresentationWindow class]] && !window.hidden) { return YES; } @@ -763,7 +784,7 @@ + (BOOL)hideAllVisibleModalPresentationViewControllerIfCan { BOOL hideAllFinally = YES; - for (UIWindow *window in UIApplication.sharedApplication.windows) { + for (UIWindow *window in UIApplication.sharedApplication.qmui_windows) { if (![window isKindOfClass:[QMUIModalPresentationWindow class]]) { continue; } diff --git a/QMUIKit/QMUIComponents/QMUIMoreOperationController.h b/QMUIKit/QMUIComponents/QMUIMoreOperationController.h index f84addaa..7c523b27 100644 --- a/QMUIKit/QMUIComponents/QMUIMoreOperationController.h +++ b/QMUIKit/QMUIComponents/QMUIMoreOperationController.h @@ -106,6 +106,7 @@ NS_ASSUME_NONNULL_BEGIN /// 弹出面板,一般在 init 完并且设置好 items 之后就调用这个接口来显示面板 - (void)showFromBottom; +- (void)showFromBottomInWindow:(nullable UIWindow *)window; /// 隐藏面板 - (void)hideToBottom; diff --git a/QMUIKit/QMUIComponents/QMUIMoreOperationController.m b/QMUIKit/QMUIComponents/QMUIMoreOperationController.m index 269b7a46..37260a1b 100644 --- a/QMUIKit/QMUIComponents/QMUIMoreOperationController.m +++ b/QMUIKit/QMUIComponents/QMUIMoreOperationController.m @@ -231,6 +231,10 @@ - (CGFloat)suitableColumnCountWithCount:(CGFloat)columnCount { } - (void)showFromBottom { + [self showFromBottomInWindow:nil]; +} + +- (void)showFromBottomInWindow:(nullable UIWindow *)window { if (self.showing || self.animating) { return; @@ -285,7 +289,7 @@ - (void)showFromBottom { }; self.animating = YES; - [modalPresentationViewController showWithAnimated:YES completion:NULL]; + [modalPresentationViewController showInWindow:window animated:YES completion:NULL]; } - (void)hideToBottom { diff --git a/QMUIKit/QMUIComponents/QMUIPopupContainerView.h b/QMUIKit/QMUIComponents/QMUIPopupContainerView.h index 2da5a690..bd6a3e4f 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupContainerView.h +++ b/QMUIKit/QMUIComponents/QMUIPopupContainerView.h @@ -110,7 +110,7 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) { /// 最小宽度(指整个控件的宽度,而不是contentView部分),默认为0 @property(nonatomic, assign) CGFloat minimumWidth UI_APPEARANCE_SELECTOR; -/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX,会在布局时被动态修改。 +/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX @property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR; /// 最小高度(指整个控件的高度,而不是contentView部分),默认为0 @@ -176,6 +176,8 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) { - (void)showWithAnimated:(BOOL)animated; - (void)showWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; + - (void)hideWithAnimated:(BOOL)animated; - (void)hideWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; - (BOOL)isShowing; diff --git a/QMUIKit/QMUIComponents/QMUIPopupContainerView.m b/QMUIKit/QMUIComponents/QMUIPopupContainerView.m index dd76738c..047fe754 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupContainerView.m +++ b/QMUIKit/QMUIComponents/QMUIPopupContainerView.m @@ -24,6 +24,7 @@ #import "QMUIAppearance.h" #import "CALayer+QMUI.h" #import "NSShadow+QMUI.h" +#import "UIApplication+QMUI.h" @interface QMUIPopupContainerViewWindow : UIWindow @@ -48,7 +49,6 @@ @interface QMUIPopupContainerView () { } @property(nonatomic, strong) QMUIPopupContainerViewWindow *popupWindow; -@property(nonatomic, weak) UIWindow *previousKeyWindow; @property(nonatomic, assign) BOOL hidesByUserTap; @end @@ -101,6 +101,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { } - (void)setBackgroundView:(UIView *)backgroundView { + NSAssert(![backgroundView isKindOfClass:UIVisualEffectView.class], @"不支持UIVisualEffectView,请使用一个UIView代替,其subview可添加UIVisualEffectView"); if (_backgroundView && _backgroundView != backgroundView) { [_backgroundView removeFromSuperview]; } @@ -379,7 +380,7 @@ - (void)setSourceBarItem:(__kindof UIBarItem *)sourceBarItem { // 每次都要重新定义 block,否则当不同的 popup 在同一个 sourceBarItem 显示,这个 block 内部得到的 weakSelf 可能是前一次的 sourceBarItem.qmui_viewLayoutDidChangeBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { if (!view.window || !weakSelf.superview) return; - UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window + UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.qmui_delegateWindow : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView]; weakSelf.sourceRect = rect; }; @@ -399,7 +400,7 @@ - (void)setSourceView:(__kindof UIView *)sourceView { __weak __typeof(self)weakSelf = self; sourceView.qmui_frameDidChangeBlock = ^(__kindof UIView * _Nonnull view, CGRect precedingFrame) { if (!view.window || !weakSelf.superview) return; - UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window + UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.qmui_delegateWindow : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView]; weakSelf.sourceRect = rect; }; @@ -530,11 +531,11 @@ - (void)layoutWithTargetRect:(CGRect)targetRect { // 上下都没有足够的空间,所以要调整maximumHeight CGFloat maximumHeightAbove = CGRectGetMinY(targetRect) - CGRectGetMinY(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.top; CGFloat maximumHeightBelow = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect); - self.maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow)); - tipSize.height = self.maximumHeight; + CGFloat maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow)); + tipSize.height = maximumHeight; _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow; - QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方"); + QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方"); } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove && !canShowAtAbove) { _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow; @@ -598,11 +599,11 @@ - (void)layoutWithTargetRect:(CGRect)targetRect { // 左右都没有足够的空间,所以要调整maximumWidth CGFloat maximumWidthLeft = CGRectGetMinX(targetRect) - CGRectGetMinX(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.left; CGFloat maximumWidthRight = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - self.distanceBetweenSource - CGRectGetMaxX(targetRect); - self.maximumWidth = MAX(self.minimumWidth, MAX(maximumWidthLeft, maximumWidthRight)); - tipSize.width = self.maximumWidth; + CGFloat maximumWidth = MAX(self.minimumWidth, MAX(maximumWidthLeft, maximumWidthRight)); + tipSize.width = maximumWidth; _currentLayoutDirection = maximumWidthLeft > maximumWidthRight ? QMUIPopupContainerViewLayoutDirectionLeft : QMUIPopupContainerViewLayoutDirectionRight; - QMUILog(NSStringFromClass(self.class), @"%@, 因为左右都不够空间,所以最大宽度被强制改为%@, 位于目标的%@", self, @(self.maximumWidth), maximumWidthLeft > maximumWidthRight ? @"左边" : @"右边"); + QMUILog(NSStringFromClass(self.class), @"%@, 因为左右都不够空间,所以最大宽度被强制改为%@, 位于目标的%@", self, @(maximumWidth), maximumWidthLeft > maximumWidthRight ? @"左边" : @"右边"); } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft && !canShowAtLeft) { _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionLeft; @@ -689,16 +690,18 @@ - (void)showWithAnimated:(BOOL)animated { } - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - + [self showInWindow:nil animated:animated completion:completion]; +} + +- (void)showInWindow:(nullable UIWindow *)window animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion { BOOL isShowingByWindowMode = NO; if (!self.superview) { - [self initPopupContainerViewWindowIfNeeded]; + [self initPopupContainerViewWindowInWindow:window]; QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController; viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; - self.previousKeyWindow = UIApplication.sharedApplication.keyWindow; - [self.popupWindow makeKeyAndVisible]; + self.popupWindow.hidden = NO; isShowingByWindowMode = YES; } else { @@ -797,17 +800,13 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - (void)hideCompletionWithWindowMode:(BOOL)windowMode completion:(void (^)(BOOL))completion { if (windowMode) { - // 恢复 keyWindow 之前做一下检查,避免类似问题 https://github.com/Tencent/QMUI_iOS/issues/90 - if (UIApplication.sharedApplication.keyWindow == self.popupWindow) { - [self.previousKeyWindow makeKeyWindow]; - } - // iOS 9 下(iOS 8 和 10 都没问题)需要主动移除,才能令 rootViewController 和 popupWindow 立即释放,不影响后续的 layout 判断,如果不加这两句,虽然 popupWindow 指针被置为 nil,但其实对象还存在,View 层级关系也还在 // https://github.com/Tencent/QMUI_iOS/issues/75 [self removeFromSuperview]; self.popupWindow.rootViewController = nil; self.popupWindow.hidden = YES; + self.popupWindow.windowScene = nil; self.popupWindow = nil; } else { self.hidden = YES; @@ -833,9 +832,10 @@ - (BOOL)isSubviewShowing:(UIView *)subview { return subview && !subview.hidden && subview.superview; } -- (void)initPopupContainerViewWindowIfNeeded { +- (void)initPopupContainerViewWindowInWindow:(nullable UIWindow *)window { if (!self.popupWindow) { - self.popupWindow = [[QMUIPopupContainerViewWindow alloc] init]; + UIWindowScene *windowScene = window.windowScene ? : UIApplication.sharedApplication.qmui_delegateWindow.windowScene; + self.popupWindow = [QMUIPopupContainerViewWindow qmui_windowWithWindowScene:windowScene]; self.popupWindow.qmui_capturesStatusBarAppearance = NO; self.popupWindow.backgroundColor = UIColorClear; self.popupWindow.windowLevel = UIWindowLevelQMUIAlertView; diff --git a/QMUIKit/QMUIComponents/QMUISearchController.m b/QMUIKit/QMUIComponents/QMUISearchController.m index 0a77d8c2..b54124c5 100644 --- a/QMUIKit/QMUIComponents/QMUISearchController.m +++ b/QMUIKit/QMUIComponents/QMUISearchController.m @@ -26,6 +26,7 @@ #import "UIViewController+QMUI.h" #import "UISearchController+QMUI.h" #import "UIGestureRecognizer+QMUI.h" +#import "UIApplication+QMUI.h" BeginIgnoreDeprecatedWarning @@ -250,7 +251,7 @@ - (void)handleSwipe:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { self.dismissBySwipe = YES; // 盖到最上面,挡住退出搜索过程中可能出现的界面闪烁 [self.snapshotView removeFromSuperview]; - [UIApplication.sharedApplication.delegate.window addSubview:self.snapshotView]; + [UIApplication.sharedApplication.qmui_delegateWindow addSubview:self.snapshotView]; QMUILogInfo(@"QMUISearchController", @"swipeGesture snapshot change superview to window"); self.active = NO; self.searchController.view.transform = CGAffineTransformIdentity; @@ -285,7 +286,7 @@ - (void)createSnapshotObjects { self.snapshotMaskView = [[UIView alloc] init]; self.snapshotMaskView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:.1]; } - self.snapshotView = [UIApplication.sharedApplication.delegate.window snapshotViewAfterScreenUpdates:NO]; + self.snapshotView = [UIApplication.sharedApplication.qmui_delegateWindow snapshotViewAfterScreenUpdates:NO]; self.snapshotMaskView.frame = self.snapshotView.bounds; [self.snapshotView addSubview:self.snapshotMaskView]; if (!self.swipeGestureRecognizer) { @@ -293,7 +294,7 @@ - (void)createSnapshotObjects { self.swipeGestureRecognizer.edges = UIRectEdgeLeft; self.swipeGestureRecognizer.delegate = self; } - [UIApplication.sharedApplication.delegate.window addGestureRecognizer:self.swipeGestureRecognizer]; + [UIApplication.sharedApplication.qmui_delegateWindow addGestureRecognizer:self.swipeGestureRecognizer]; } - (void)resetSnapshotObjects { @@ -305,7 +306,7 @@ - (void)cleanSnapshotObjects { [self.snapshotView removeFromSuperview]; [self.snapshotMaskView removeFromSuperview]; self.snapshotView = nil; - [UIApplication.sharedApplication.delegate.window removeGestureRecognizer:self.swipeGestureRecognizer]; + [UIApplication.sharedApplication.qmui_delegateWindow removeGestureRecognizer:self.swipeGestureRecognizer]; QMUILogInfo(@"QMUISearchController", @"swipeGesture clean all objects"); } diff --git a/QMUIKit/QMUIComponents/QMUITextView.m b/QMUIKit/QMUIComponents/QMUITextView.m index 0f319661..d7e50119 100644 --- a/QMUIKit/QMUIComponents/QMUITextView.m +++ b/QMUIKit/QMUIComponents/QMUITextView.m @@ -25,9 +25,6 @@ /// 系统 textView 默认的字号大小,用于 placeholder 默认的文字大小。实测得到,请勿修改。 const CGFloat kSystemTextViewDefaultFontPointSize = 12.0f; -/// 当系统的 textView.textContainerInset 为 UIEdgeInsetsZero 时,文字与 textView 边缘的间距。实测得到,请勿修改(在输入框font大于13时准确,小于等于12时,y有-1px的偏差)。 -const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5}; - // 私有的类,专用于实现 QMUITextViewDelegate,避免 self.delegate = self 的写法(以前是 QMUITextView 自己实现了 delegate) @interface _QMUITextViewDelegator : NSObject @@ -304,7 +301,16 @@ - (CGSize)sizeThatFits:(CGSize)size { } - (UIEdgeInsets)allInsets { - return UIEdgeInsetsConcat(UIEdgeInsetsConcat(UIEdgeInsetsConcat(self.textContainerInset, self.placeholderMargins), kSystemTextViewFixTextInsets), self.adjustedContentInset); + CGFloat padding = self.textContainer.lineFragmentPadding; + UIEdgeInsets textContainerInset = self.textContainerInset; + /// https://github.com/Tencent/QMUI_iOS/issues/1601 + if (@available(iOS 17.0, *)) { + textContainerInset.top = MAX(textContainerInset.top, 0); + textContainerInset.left = MAX(textContainerInset.left, 0); + textContainerInset.bottom = MAX(textContainerInset.bottom, 0); + textContainerInset.right = MAX(textContainerInset.right, 0); + } + return UIEdgeInsetsConcat(UIEdgeInsetsConcat(UIEdgeInsetsConcat(textContainerInset, self.placeholderMargins), UIEdgeInsetsMake(0, padding, 0, padding)), self.adjustedContentInset); } - (void)setFrame:(CGRect)frame { diff --git a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m index d73a5326..d2ae0ffc 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m +++ b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m @@ -18,6 +18,7 @@ #import "UIViewController+QMUITheme.h" #import "QMUIThemePrivate.h" #import "UITraitCollection+QMUI.h" +#import "UIApplication+QMUI.h" NSString *const QMUIThemeDidChangeNotification = @"QMUIThemeDidChangeNotification"; @@ -58,7 +59,7 @@ - (void)handleUserInterfaceStyleWillChangeEvent:(UITraitCollection *)traitCollec - (void)setRespondsSystemStyleAutomatically:(BOOL)respondsSystemStyleAutomatically { _respondsSystemStyleAutomatically = respondsSystemStyleAutomatically; if (_respondsSystemStyleAutomatically && self.identifierForTrait) { - self.currentThemeIdentifier = self.identifierForTrait([UITraitCollection currentTraitCollection]); + self.currentThemeIdentifier = self.identifierForTrait(UIScreen.mainScreen.traitCollection); } } @@ -137,7 +138,7 @@ - (void)removeTheme:(NSObject *)theme { - (void)notifyThemeChanged { [[NSNotificationCenter defaultCenter] postNotificationName:QMUIThemeDidChangeNotification object:self]; - [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + [UIApplication.sharedApplication.qmui_windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { if (!window.hidden && window.alpha > 0.01 && window.rootViewController) { [window.rootViewController qmui_themeDidChangeByManager:self identifier:self.currentThemeIdentifier theme:self.currentTheme]; diff --git a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m index 558dcae2..2f394996 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m +++ b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m @@ -397,6 +397,19 @@ + (void)load { }; }); + OverrideImplementation([UIView class], @selector(setBackgroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIColor *backgroundColor) { + + /// https://github.com/Tencent/QMUI_iOS/issues/1597 + selfObject.layer.qcl_originalBackgroundColor = backgroundColor.qmui_isQMUIDynamicColor ? backgroundColor : nil; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, backgroundColor); + }; + }); + OverrideImplementation([CALayer class], @selector(setBorderColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(CALayer *selfObject, CGColorRef color) { diff --git a/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m b/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m index 0be2e6df..463905c5 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m +++ b/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m @@ -104,7 +104,7 @@ - (CGColorRef)CGColor { CGColorRef cgColor = CGColorCreate(spaceRef, (CGFloat[]){rawColor.qmui_red, rawColor.qmui_green, rawColor.qmui_blue, rawColor.qmui_alpha}); CGColorSpaceRelease(spaceRef); - [(__bridge id)(cgColor) qmui_bindObject:self forKey:QMUICGColorOriginalColorBindKey]; + [(__bridge id)(cgColor) qmui_bindObjectWeakly:self forKey:QMUICGColorOriginalColorBindKey]; return (CGColorRef)CFAutorelease(cgColor); } diff --git a/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m b/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m index 32f91c15..9376d59f 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m +++ b/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m @@ -139,6 +139,15 @@ - (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__k BOOL isValidatedImage = [value isKindOfClass:QMUIThemeImage.class] && (!manager || [((QMUIThemeImage *)value).managerName isEqual:manager.name]); BOOL isValidatedEffect = [value isKindOfClass:QMUIThemeVisualEffect.class] && (!manager || [((QMUIThemeVisualEffect *)value).managerName isEqual:manager.name]); BOOL isOtherObject = ![value isKindOfClass:UIColor.class] && ![value isKindOfClass:UIImage.class] && ![value isKindOfClass:UIVisualEffect.class];// 支持所有非 color、image、effect 的其他对象,例如 NSAttributedString + + // iOS 17,切换主题后图片没有更新 + // https://github.com/Tencent/QMUI_iOS/issues/1507 + if (@available(iOS 17.0, *)) { + if (isValidatedImage) { + value = [(QMUIThemeImage *)value copy]; + } + } + if (isOtherObject || isValidatedColor || isValidatedImage || isValidatedEffect) { [self performSelector:setter withObject:value]; } diff --git a/QMUIKit/QMUIComponents/QMUITips.h b/QMUIKit/QMUIComponents/QMUITips.h index 3aeb0b4e..83db3849 100644 --- a/QMUIKit/QMUIComponents/QMUITips.h +++ b/QMUIKit/QMUIComponents/QMUITips.h @@ -17,10 +17,7 @@ #import "QMUIToastView.h" // 自动计算秒数的标志符,在 delay 里面赋值 QMUITipsAutomaticallyHideToastSeconds 即可通过自动计算 tips 消失的秒数 -extern const NSInteger QMUITipsAutomaticallyHideToastSeconds; - -/// 默认的 parentView -#define DefaultTipsParentView (UIApplication.sharedApplication.delegate.window) +UIKIT_EXTERN const NSInteger QMUITipsAutomaticallyHideToastSeconds; /** * 简单封装了 QMUIToastView,支持弹出纯文本、loading、succeed、error、info 等五种 tips。如果这些接口还满足不了业务的需求,可以通过 QMUITips 的分类自行添加接口。 diff --git a/QMUIKit/QMUIComponents/QMUITips.m b/QMUIKit/QMUIComponents/QMUITips.m index 45aa0099..909ce23f 100644 --- a/QMUIKit/QMUIComponents/QMUITips.m +++ b/QMUIKit/QMUIComponents/QMUITips.m @@ -18,9 +18,13 @@ #import "QMUIToastContentView.h" #import "QMUIToastBackgroundView.h" #import "NSString+QMUI.h" +#import "UIApplication+QMUI.h" const NSInteger QMUITipsAutomaticallyHideToastSeconds = -1; +/// 默认的 parentView +#define QMUIDefaultTipsParentView (UIApplication.sharedApplication.qmui_delegateWindow) + @interface QMUITips () @property(nonatomic, strong) UIView *contentCustomView; @@ -199,11 +203,11 @@ + (QMUITips *)showLoading:(NSString *)text detailText:(NSString *)detailText inV } + (QMUITips *)showWithText:(nullable NSString *)text { - return [self showWithText:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showWithText:text detailText:nil inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText { - return [self showWithText:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showWithText:text detailText:detailText inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showWithText:(NSString *)text inView:(UIView *)view { @@ -225,11 +229,11 @@ + (QMUITips *)showWithText:(NSString *)text detailText:(NSString *)detailText in } + (QMUITips *)showSucceed:(nullable NSString *)text { - return [self showSucceed:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showSucceed:text detailText:nil inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText { - return [self showSucceed:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showSucceed:text detailText:detailText inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showSucceed:(NSString *)text inView:(UIView *)view { @@ -251,11 +255,11 @@ + (QMUITips *)showSucceed:(NSString *)text detailText:(NSString *)detailText inV } + (QMUITips *)showError:(nullable NSString *)text { - return [self showError:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showError:text detailText:nil inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText { - return [self showError:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showError:text detailText:detailText inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showError:(NSString *)text inView:(UIView *)view { @@ -277,11 +281,11 @@ + (QMUITips *)showError:(NSString *)text detailText:(NSString *)detailText inVie } + (QMUITips *)showInfo:(nullable NSString *)text { - return [self showInfo:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showInfo:text detailText:nil inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText { - return [self showInfo:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; + return [self showInfo:text detailText:detailText inView:QMUIDefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; } + (QMUITips *)showInfo:(NSString *)text inView:(UIView *)view { diff --git a/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m b/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m index a926adad..cbef6c45 100644 --- a/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m +++ b/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m @@ -15,6 +15,7 @@ #import "QMUIWindowSizeMonitor.h" #import "QMUICore.h" #import "NSPointerArray+QMUI.h" +#import "UIApplication+QMUI.h" @interface NSObject (QMUIWindowSizeMonitor_Private) @@ -44,7 +45,7 @@ - (void)qwsm_addSizeObserver:(NSObject *)observer; @implementation NSObject (QMUIWindowSizeMonitor) - (void)qmui_addSizeObserverForMainWindow:(QMUIWindowSizeObserverHandler)handler { - [self qmui_addSizeObserverForWindow:UIApplication.sharedApplication.delegate.window handler:handler]; + [self qmui_addSizeObserverForWindow:UIApplication.sharedApplication.qmui_delegateWindow handler:handler]; } - (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSizeObserverHandler)handler { diff --git a/QMUIKit/QMUICore/QMUICommonDefines.h b/QMUIKit/QMUICore/QMUICommonDefines.h index 8f77b6d4..17361d95 100644 --- a/QMUIKit/QMUICore/QMUICommonDefines.h +++ b/QMUIKit/QMUICore/QMUICommonDefines.h @@ -81,6 +81,11 @@ #define IOS18_SDK_ALLOWED YES #endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 260000 +/// 当前编译使用的 Base SDK 版本为 iOS 26.0 及以上 +#define IOS26_SDK_ALLOWED YES +#endif + #pragma mark - Clang #define ArgumentToString(macro) #macro diff --git a/QMUIKit/QMUICore/QMUIConfiguration.m b/QMUIKit/QMUICore/QMUIConfiguration.m index ef74deb8..701ba05c 100644 --- a/QMUIKit/QMUICore/QMUIConfiguration.m +++ b/QMUIKit/QMUICore/QMUIConfiguration.m @@ -1029,7 +1029,7 @@ - (void)setDefaultStatusBarStyle:(UIStatusBarStyle)defaultStatusBarStyle { - (NSArray *)appearanceUpdatingViewControllersOfClasses:(NSArray> *)classes { if (!classes.count) return nil; NSMutableArray *viewControllers = [NSMutableArray array]; - [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + [UIApplication.sharedApplication.qmui_windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { if (window.rootViewController) { [viewControllers addObjectsFromArray:[window.rootViewController qmui_existingViewControllersOfClasses:classes]]; } diff --git a/QMUIKit/QMUICore/QMUIHelper.h b/QMUIKit/QMUICore/QMUIHelper.h index 1129501b..46946606 100644 --- a/QMUIKit/QMUICore/QMUIHelper.h +++ b/QMUIKit/QMUICore/QMUIHelper.h @@ -274,6 +274,9 @@ NS_ASSUME_NONNULL_BEGIN */ @property(class, nonatomic, assign, readonly) BOOL canUpdateAppearance; +/// iOS 26 液态玻璃 +@property (class, nonatomic, readonly) BOOL isUsedLiquidGlass; + @end @interface QMUIHelper (Animation) diff --git a/QMUIKit/QMUICore/QMUIHelper.m b/QMUIKit/QMUICore/QMUIHelper.m index c313d23b..5525d690 100644 --- a/QMUIKit/QMUICore/QMUIHelper.m +++ b/QMUIKit/QMUICore/QMUIHelper.m @@ -24,6 +24,9 @@ #import #import #import +#import "UIApplication+QMUI.h" +#import "QMUICommonDefines.h" +#import "UIWindow+QMUI.h" NSString *const kQMUIResourcesBundleName = @"QMUIResources"; @@ -546,7 +549,7 @@ + (BOOL)isNotchedScreen { UIEdgeInsets peripheryInsets = UIEdgeInsetsZero; [[UIScreen mainScreen] qmui_performSelector:peripheryInsetsSelector withPrimitiveReturnValue:&peripheryInsets]; if (peripheryInsets.bottom <= 0) { - UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + UIWindow *window = [UIWindow qmui_windowWithWindowScene:UIApplication.sharedApplication.qmui_delegateWindow.windowScene]; peripheryInsets = window.safeAreaInsets; if (peripheryInsets.bottom <= 0) { // 使用一个强制竖屏的 rootViewController,避免一个仅支持竖屏的 App 在横屏启动时会受这里创建的 window 的影响,导致状态栏、safeAreaInsets 等错乱 @@ -755,7 +758,7 @@ + (CGFloat)preferredLayoutAsSimilarScreenWidthForIPad { @([self screenSizeFor58Inch].width), @([self screenSizeFor40Inch].width)]; preferredLayoutWidth = SCREEN_WIDTH; - UIWindow *window = UIApplication.sharedApplication.delegate.window ?: [[UIWindow alloc] init];// iOS 9 及以上的系统,新 init 出来的 window 自动被设置为当前 App 的宽度 + UIWindow *window = UIApplication.sharedApplication.qmui_delegateWindow ?: [[UIWindow alloc] init];// iOS 9 及以上的系统,新 init 出来的 window 自动被设置为当前 App 的宽度 CGFloat windowWidth = CGRectGetWidth(window.bounds); for (NSInteger i = 0; i < widths.count; i++) { if (windowWidth <= widths[i].qmui_CGFloatValue) { @@ -1059,7 +1062,7 @@ + (CGSize)applicationSize { CGSize applicationSize = CGSizeMake(applicationFrame.size.width + applicationFrame.origin.x, applicationFrame.size.height + applicationFrame.origin.y); if (CGSizeEqualToSize(applicationSize, CGSizeZero)) { // 实测 MacCatalystApp 通过 [UIScreen mainScreen].applicationFrame 拿不到大小,这里做一下保护 - UIWindow *window = UIApplication.sharedApplication.delegate.window; + UIWindow *window = UIApplication.sharedApplication.qmui_delegateWindow; if (window) { applicationSize = window.bounds.size; } else { @@ -1136,13 +1139,13 @@ + (CGFloat)navigationBarMaxYConstant { @implementation QMUIHelper (UIApplication) + (void)dimmedApplicationWindow { - UIWindow *window = UIApplication.sharedApplication.delegate.window; + UIWindow *window = UIApplication.sharedApplication.qmui_delegateWindow; window.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed; [window tintColorDidChange]; } + (void)resetDimmedApplicationWindow { - UIWindow *window = UIApplication.sharedApplication.delegate.window; + UIWindow *window = UIApplication.sharedApplication.qmui_delegateWindow; window.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic; [window tintColorDidChange]; } @@ -1164,6 +1167,23 @@ + (BOOL)canUpdateAppearance { return YES; } ++ (BOOL)isUsedLiquidGlass { + static BOOL result = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ +#ifdef IOS26_SDK_ALLOWED + if (@available(iOS 26.0, *)) { + result = ![[NSBundle.mainBundle objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"] boolValue]; + } else { + result = NO; + } +#else + result = NO; +#endif + }); + return result; +} + @end @implementation QMUIHelper (Animation) diff --git a/QMUIKit/UIKitExtensions/NSObject+QMUI.m b/QMUIKit/UIKitExtensions/NSObject+QMUI.m index 9fd21b90..1ec4475d 100644 --- a/QMUIKit/UIKitExtensions/NSObject+QMUI.m +++ b/QMUIKit/UIKitExtensions/NSObject+QMUI.m @@ -520,8 +520,18 @@ + (void)load { originSelectorIMP = (id (*)(id, SEL, NSExceptionName name, NSString *, ...))originalIMPProvider(); va_list args; va_start(args, format); - NSString *reason = [[NSString alloc] initWithFormat:format arguments:args]; - originSelectorIMP(selfObject, originCMD, raise, reason); + NSString *reason = [[NSString alloc] initWithFormat:format arguments:args]; + BOOL shouldCallSuper = YES; + // https://github.com/Tencent/QMUI_iOS/issues/1680 + if (QMUIHelper.isUsedLiquidGlass) { + if (raise == NSInternalInconsistencyException && [reason hasPrefix:@"The layout constraints still need update after sending -updateConstraints to <_UINavigationBarTitleControl"]) { + QMUILogWarn(@"NSObject (QMUI)", @"iOS 26.0会因约束问题触发_UINavigationBarTitleControl的 NSException,详情见:https://github.com/Tencent/QMUI_iOS/issues/1680"); + shouldCallSuper = NO; + } + } + if (shouldCallSuper) { + originSelectorIMP(selfObject, originCMD, raise, reason); + } va_end(args); }; }); diff --git a/QMUIKit/UIKitExtensions/UIApplication+QMUI.h b/QMUIKit/UIKitExtensions/UIApplication+QMUI.h index 94250659..fd3df985 100644 --- a/QMUIKit/UIKitExtensions/UIApplication+QMUI.h +++ b/QMUIKit/UIKitExtensions/UIApplication+QMUI.h @@ -20,6 +20,12 @@ NS_ASSUME_NONNULL_BEGIN /// 判断当前的 App 是否已经完全启动 @property(nonatomic, assign, readonly) BOOL qmui_didFinishLaunching; + +@property (nonatomic, readonly) NSArray<__kindof UIWindow *> *qmui_windows; + +@property (nullable, nonatomic, readonly) __kindof UIWindow *qmui_keyWindow; +@property (nullable, nonatomic, readonly) __kindof UIWindow *qmui_delegateWindow; + @end NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/UIApplication+QMUI.m b/QMUIKit/UIKitExtensions/UIApplication+QMUI.m index 36129ad3..934c0d7a 100644 --- a/QMUIKit/UIKitExtensions/UIApplication+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIApplication+QMUI.m @@ -45,4 +45,58 @@ - (void)qmui_handleDidFinishLaunchingNotification:(NSNotification *)notification [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidFinishLaunchingNotification object:nil]; } +- (NSArray<__kindof UIWindow *> *)qmui_windows { + __block NSArray *windows = nil; + [self.connectedScenes enumerateObjectsUsingBlock:^(UIScene *scene, BOOL *stop) { + if ([scene isKindOfClass:UIWindowScene.class] && [scene.session.role isEqualToString:UIWindowSceneSessionRoleApplication]) { + windows = [(UIWindowScene *)scene windows]; + *stop = YES; + } + }]; + if (!windows || windows.count == 0) { + windows = self.windows; + } + return windows ? : @[]; +} + +- (nullable __kindof UIWindow *)qmui_keyWindow { + __block UIWindow *keyWindow = nil; + [self.connectedScenes enumerateObjectsUsingBlock:^(UIScene *scene, BOOL *stop) { + if ([scene isKindOfClass:UIWindowScene.class] && [scene.session.role isEqualToString:UIWindowSceneSessionRoleApplication]) { + [[(UIWindowScene *)scene windows] enumerateObjectsUsingBlock:^(UIWindow *window, NSUInteger idx, BOOL *substop) { + if (window.isKeyWindow && !window.isHidden) { + keyWindow = window; + *substop = YES; + } + }]; + *stop = YES; + } + }]; + if (!keyWindow) { + BeginIgnoreDeprecatedWarning + keyWindow = self.keyWindow; + EndIgnoreDeprecatedWarning + } + if (!keyWindow) { + keyWindow = self.qmui_delegateWindow; + } + return keyWindow; +} + +- (nullable __kindof UIWindow *)qmui_delegateWindow { + __block UIWindow *delegateWindow = nil; + [self.connectedScenes enumerateObjectsUsingBlock:^(UIScene *scene, BOOL *stop) { + if ([scene isKindOfClass:UIWindowScene.class] && [scene.session.role isEqualToString:UIWindowSceneSessionRoleApplication]) { + if ([scene.delegate respondsToSelector:@selector(window)]) { + delegateWindow = [scene.delegate performSelector:@selector(window)]; + *stop = YES; + } + } + }]; + if (!delegateWindow && [self.delegate respondsToSelector:@selector(window)]) { + delegateWindow = [self.delegate performSelector:@selector(window)]; + } + return delegateWindow; +} + @end diff --git a/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m b/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m index 4812e800..465409e4 100644 --- a/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m @@ -35,9 +35,27 @@ + (void)load { }); // -[UITabBarItem setView:] - ExtendImplementationOfVoidMethodWithSingleArgument([UITabBarItem class], @selector(setView:), UIView *, ^(UITabBarItem *selfObject, UIView *firstArgv) { - [UIBarItem setView:firstArgv inBarItem:selfObject]; - }); + if (@available(iOS 18.0, *)) { + OverrideImplementation([UITabBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITabBar *selfObject, NSArray *items, BOOL animated) { + + // call super + void (*originSelectorIMP)(id, SEL, NSArray *, BOOL); + originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, items, animated); + + [items enumerateObjectsUsingBlock:^(UITabBarItem *item, NSUInteger idx, BOOL *stop) { + if (item.qmui_view) { + [UIBarItem setView:item.qmui_view inBarItem:item]; + } + }]; + }; + }); + } else { + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBarItem class], @selector(setView:), UIView *, ^(UITabBarItem *selfObject, UIView *firstArgv) { + [UIBarItem setView:firstArgv inBarItem:selfObject]; + }); + } }); } diff --git a/QMUIKit/UIKitExtensions/UIColor+QMUI.m b/QMUIKit/UIKitExtensions/UIColor+QMUI.m index af1140cd..53b3dbd6 100644 --- a/QMUIKit/UIKitExtensions/UIColor+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIColor+QMUI.m @@ -341,7 +341,7 @@ + (void)load { result = CGColorCreate(spaceRef, (CGFloat[]){color.qmui_red, color.qmui_green, color.qmui_blue, color.qmui_alpha}); CGColorSpaceRelease(spaceRef); - [(__bridge id)(result) qmui_bindObject:selfObject forKey:QMUICGColorOriginalColorBindKey]; + [(__bridge id)(result) qmui_bindObjectWeakly:selfObject forKey:QMUICGColorOriginalColorBindKey]; return (CGColorRef)CFAutorelease(result); } diff --git a/QMUIKit/UIKitExtensions/UIInterface+QMUI.m b/QMUIKit/UIKitExtensions/UIInterface+QMUI.m index 18b01795..c18f36d2 100644 --- a/QMUIKit/UIKitExtensions/UIInterface+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIInterface+QMUI.m @@ -15,6 +15,7 @@ #import "UIInterface+QMUI.h" #import "QMUICore.h" +#import "UIApplication+QMUI.h" @implementation QMUIHelper (QMUI_Interface) @@ -189,7 +190,7 @@ - (BOOL)qmui_rotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrien __block BOOL result = YES; UIInterfaceOrientationMask mask = 1 << interfaceOrientation; - UIWindow *window = self.view.window ?: UIApplication.sharedApplication.delegate.window; + UIWindow *window = self.view.window ?: UIApplication.sharedApplication.qmui_delegateWindow; [window.windowScene requestGeometryUpdateWithPreferences:[[UIWindowSceneGeometryPreferencesIOS alloc] initWithInterfaceOrientations:mask] errorHandler:^(NSError * _Nonnull error) { if (error) { result = NO; diff --git a/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m b/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m index 572dba84..5f2dac86 100644 --- a/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m @@ -15,6 +15,7 @@ #import "UIMenuController+QMUI.h" #import "QMUICore.h" #import "NSArray+QMUI.h" +#import "UIApplication+QMUI.h" @implementation UIMenuController (QMUI) @@ -110,7 +111,7 @@ + (UIWindow *)qmuimc_menuControllerWindow { if (kMenuControllerWindow && !kMenuControllerWindow.hidden) { return kMenuControllerWindow; } - [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + [UIApplication.sharedApplication.qmui_windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { NSString *windowString = [NSString stringWithFormat:@"UI%@%@", @"Text", @"EffectsWindow"]; if ([window isKindOfClass:NSClassFromString(windowString)] && !window.hidden) { if (@available(iOS 16.0, *)) { @@ -136,8 +137,8 @@ + (UIWindow *)qmuimc_menuControllerWindow { + (UIWindow *)qmuimc_firstResponderWindowExceptMainWindow { __block UIWindow *resultWindow = nil; - [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { - if (window != UIApplication.sharedApplication.delegate.window) { + [UIApplication.sharedApplication.qmui_windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + if (window != UIApplication.sharedApplication.qmui_delegateWindow) { UIResponder *responder = [UIMenuController qmuimc_findFirstResponderInView:window]; if (responder) { resultWindow = window; diff --git a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m index 8d82a777..94dc1f12 100644 --- a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m @@ -328,7 +328,7 @@ + (void)load { } }; }); - +#ifdef DEBUG // 尚未应用 UIAppearance 就已经修改 bar 的样式的场景,可能导致 bar 样式无法与全局保持一致,所以这里做个提醒 // https://github.com/Tencent/QMUI_iOS/issues/1451 // - [UINavigationBar setStandardAppearance:] @@ -339,27 +339,41 @@ + (void)load { void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *); originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, firstArgv); - + // 这里只希望识别 UINavigationController 自带的 navigationBar,不希望处理业务自己 new 的 bar,所以用 superview 是否为 UILayoutContainerView 来作为判断条件。 - BOOL isSystemBar = [NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"]; + BOOL isSystemBar = [NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"] && [selfObject.superview.qmui_viewController isKindOfClass:UINavigationController.class]; BOOL alreadyMoveToWindow = !!selfObject.window; BOOL isPresenting = NO; if (!alreadyMoveToWindow) { UINavigationController *nav = [selfObject.qmui_viewController isKindOfClass:UINavigationController.class] ? selfObject.qmui_viewController : nil; - isPresenting = nav && nav.presentedViewController; + isPresenting = nav && nav.presentedViewController; } if (isSystemBar && !alreadyMoveToWindow && !isPresenting) { QMUIAssert(NO, @"UINavigationBar (QMUI)", @"试图在 UINavigationBar 尚未添加到 window 上时就修改它的样式,可能导致 UINavigationBar 的样式无法与全局保持一致。"); } }; }); +#endif } #endif }); } - (UIView *)qmui_contentView { - return [self valueForKeyPath:@"visualProvider.contentView"]; + if (QMUIHelper.isUsedLiquidGlass) { + for (UIView *subview in self.subviews) { + static NSString *clsString = nil; + if (!clsString) { + clsString = [NSString stringWithFormat:@"%@.%@%@", @"UIKit", @"NavigationBar", @"ContentView"]; + } + if ([subview isKindOfClass:NSClassFromString(clsString)]) { + return subview; + } + } + return nil; + } else { + return [self valueForKeyPath:@"visualProvider.contentView"]; + } } - (void)qmuinb_fixTitleViewLayoutInIOS16 { diff --git a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m index f5a15064..342d037a 100644 --- a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m @@ -245,7 +245,11 @@ + (void)load { QMUINavigationAction action = selfObject.qmui_navigationAction; if (action != QMUINavigationActionUnknow) { - QMUILogWarn(@"UINavigationController (QMUI)", @"popViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + if (QMUIHelper.isUsedLiquidGlass) { + // iOS 26液态玻璃下,转场动画可以被打断 + } else { + QMUILogWarn(@"UINavigationController (QMUI)", @"popViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + } } BOOL willPopActually = selfObject.viewControllers.count > 1 && action == QMUINavigationActionUnknow;// 系统文档里说 rootViewController 是不能被 pop 的,当只剩下 rootViewController 时当前方法什么事都不会做 @@ -321,7 +325,11 @@ + (void)load { QMUINavigationAction action = selfObject.qmui_navigationAction; if (action != QMUINavigationActionUnknow) { - QMUILogWarn(@"UINavigationController (QMUI)", @"popToViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, currentViewControllers = %@, viewController = %@", selfObject.viewControllers, viewController); + if (QMUIHelper.isUsedLiquidGlass) { + // iOS 26液态玻璃下,转场动画可以被打断 + } else { + QMUILogWarn(@"UINavigationController (QMUI)", @"popToViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, currentViewControllers = %@, viewController = %@", selfObject.viewControllers, viewController); + } } BOOL willPopActually = selfObject.viewControllers.count > 1 && [selfObject.viewControllers containsObject:viewController] && selfObject.topViewController != viewController && action == QMUINavigationActionUnknow;// 系统文档里说 rootViewController 是不能被 pop 的,当只剩下 rootViewController 时当前方法什么事都不会做 @@ -368,7 +376,11 @@ + (void)load { QMUINavigationAction action = selfObject.qmui_navigationAction; if (action != QMUINavigationActionUnknow) { - QMUILogWarn(@"UINavigationController (QMUI)", @"popToRootViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + if (QMUIHelper.isUsedLiquidGlass) { + // iOS 26液态玻璃下,转场动画可以被打断 + } else { + QMUILogWarn(@"UINavigationController (QMUI)", @"popToRootViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + } } BOOL willPopActually = selfObject.viewControllers.count > 1 && action == QMUINavigationActionUnknow; diff --git a/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m b/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m index 4e50a0b5..503a57cb 100644 --- a/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m @@ -40,46 +40,86 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - void (^setupCancelButtonBlock)(UISearchBar *, UIButton *) = ^void(UISearchBar *searchBar, UIButton *cancelButton) { - if (searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { - cancelButton.enabled = YES; - } - - if (cancelButton && searchBar.qmui_cancelButtonFont) { - cancelButton.titleLabel.font = searchBar.qmui_cancelButtonFont; - } - - if (cancelButton && !cancelButton.qmui_frameWillChangeBlock) { - __weak __typeof(searchBar)weakSearchBar = searchBar; - cancelButton.qmui_frameWillChangeBlock = ^CGRect(UIButton *aCancelButton, CGRect followingFrame) { - return [weakSearchBar qmuisb_adjustCancelButtonFrame:followingFrame]; + if (QMUIHelper.isUsedLiquidGlass) { + ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString(@"_UISearchBarVisualProviderIOS"), NSSelectorFromString(@"setUpSearchField"), ^(NSObject *selfObject) { + UISearchBar *searchBar = [selfObject qmui_valueForKey:@"_searchBar"]; + QMUIAssert([searchBar isKindOfClass:UISearchBar.class], @"UISearchBar (QMUI)", @"Can not find UISearchBar"); + if (![searchBar isKindOfClass:UISearchBar.class]) { + return; + } + if (searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { + BOOL alwaysEnableCancelButton = YES; + [selfObject qmui_performSelector:NSSelectorFromString(@"setShowsClearButtonWhenEmpty:") withArguments:&alwaysEnableCancelButton, nil]; + } + }); + OverrideImplementation([UISearchTextField class], @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchTextField *selfObject, UIView *subview) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, subview); + + if (![subview isKindOfClass:NSClassFromString(@"_UITextFieldClearButton")]) { + return; + } + UISearchBar *searchBar = (UISearchBar *)selfObject.superview.superview.superview; + QMUIAssert([searchBar isKindOfClass:UISearchBar.class], @"UISearchBar (QMUI)", @"Can not find UISearchBar from cancelButton"); + if (![searchBar isKindOfClass:UISearchBar.class]) { + return; + } + UIButton *cancelButton = [searchBar qmui_cancelButton]; + if (cancelButton && searchBar.qmui_cancelButtonFont) { + cancelButton.titleLabel.font = searchBar.qmui_cancelButtonFont; + } + if (cancelButton && !cancelButton.qmui_frameWillChangeBlock) { + __weak __typeof(searchBar)weakSearchBar = searchBar; + cancelButton.qmui_frameWillChangeBlock = ^CGRect(UIButton *aCancelButton, CGRect followingFrame) { + return [weakSearchBar qmuisb_adjustCancelButtonFrame:followingFrame]; + }; + } }; - } - }; - - // iOS 13 开始 UISearchBar 内部的输入框、取消按钮等 subviews 都由这个 class 创建、管理 - ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString(@"_UISearchBarVisualProviderIOS"), NSSelectorFromString(@"setUpCancelButton"), ^(NSObject *selfObject) { - UIButton *cancelButton = [selfObject qmui_valueForKey:@"cancelButton"]; - UISearchBar *searchBar = (UISearchBar *)cancelButton.superview.superview.superview; - QMUIAssert([searchBar isKindOfClass:UISearchBar.class], @"UISearchBar (QMUI)", @"Can not find UISearchBar from cancelButton"); - setupCancelButtonBlock(searchBar, cancelButton); - }); - - OverrideImplementation(NSClassFromString(@"UINavigationButton"), @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIButton *selfObject, BOOL firstArgv) { + }); + } else { + // iOS 13 开始 UISearchBar 内部的输入框、取消按钮等 subviews 都由这个 class 创建、管理 + ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString(@"_UISearchBarVisualProviderIOS"), NSSelectorFromString(@"setUpCancelButton"), ^(NSObject *selfObject) { + UIButton *cancelButton = [selfObject qmui_valueForKey:@"cancelButton"]; + UISearchBar *searchBar = (UISearchBar *)cancelButton.superview.superview.superview; + QMUIAssert([searchBar isKindOfClass:UISearchBar.class], @"UISearchBar (QMUI)", @"Can not find UISearchBar from cancelButton"); + if (![searchBar isKindOfClass:UISearchBar.class]) { + return; + } + if (searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { + cancelButton.enabled = YES; + } - UISearchBar *searchBar = (UISearchBar *)selfObject.superview.superview.superview;; - if ([searchBar isKindOfClass:UISearchBar.class] && searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { - firstArgv = YES; + if (cancelButton && searchBar.qmui_cancelButtonFont) { + cancelButton.titleLabel.font = searchBar.qmui_cancelButtonFont; } - // call super - void (*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); - }; - }); - + if (cancelButton && !cancelButton.qmui_frameWillChangeBlock) { + __weak __typeof(searchBar)weakSearchBar = searchBar; + cancelButton.qmui_frameWillChangeBlock = ^CGRect(UIButton *aCancelButton, CGRect followingFrame) { + return [weakSearchBar qmuisb_adjustCancelButtonFrame:followingFrame]; + }; + } + }); + OverrideImplementation(NSClassFromString(@"UINavigationButton"), @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIButton *selfObject, BOOL firstArgv) { + + UISearchBar *searchBar = (UISearchBar *)selfObject.superview.superview.superview;; + if ([searchBar isKindOfClass:UISearchBar.class] && searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { + firstArgv = YES; + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + } + ExtendImplementationOfVoidMethodWithSingleArgument([UISearchBar class], @selector(setPlaceholder:), NSString *, (^(UISearchBar *selfObject, NSString *placeholder) { if (selfObject.qmui_placeholderColor || selfObject.qmui_font) { NSMutableAttributedString *string = selfObject.searchTextField.attributedPlaceholder.mutableCopy; @@ -334,8 +374,12 @@ - (UIFont *)qmui_font { } - (UIButton *)qmui_cancelButton { - UIButton *cancelButton = [self qmui_valueForKey:@"cancelButton"]; - return cancelButton; + if (QMUIHelper.isUsedLiquidGlass) { + return [self.searchTextField qmui_valueForKey:@"_clearButton"]; + } else { + UIButton *cancelButton = [self qmui_valueForKey:@"cancelButton"]; + return cancelButton; + } } static char kAssociatedObjectKey_cancelButtonFont; diff --git a/QMUIKit/UIKitExtensions/UISlider+QMUI.m b/QMUIKit/UIKitExtensions/UISlider+QMUI.m index 70747401..72b7da68 100644 --- a/QMUIKit/UIKitExtensions/UISlider+QMUI.m +++ b/QMUIKit/UIKitExtensions/UISlider+QMUI.m @@ -43,8 +43,21 @@ - (UIView *)qmui_thumbView { } if (!slider) return nil; - UIView *thumbView = [slider qmui_valueForKey:@"thumbView"] ?: [slider qmui_valueForKey:@"innerThumbView"]; - return thumbView; + if (QMUIHelper.isUsedLiquidGlass) { + for (UIView *subview in slider.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"_UILiquidLensView")]) { + for (UIImageView *imageView in subview.subviews) { + if ([imageView isKindOfClass:UIImageView.class]) { + return imageView; + } + } + } + } + return nil; + } else { + UIView *thumbView = [slider qmui_valueForKey:@"thumbView"] ?: [slider qmui_valueForKey:@"innerThumbView"]; + return thumbView; + } } static char kAssociatedObjectKey_trackHeight; @@ -196,6 +209,8 @@ - (void)setQmui_numberOfSteps:(NSUInteger)numberOfSteps { [self.qmuisl_stepControls removeObjectAtIndex:i]; } } + [self qmuisl_updateStepControls]; + if (self.qmui_stepControlConfiguration) { [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { self.qmui_stepControlConfiguration(self, obj, idx); @@ -243,6 +258,8 @@ - (void)qmuisl_handleValueChanged:(UISlider *)slider { NSUInteger step = [slider qmuisl_stepWithValue:slider.value]; if (step != slider.qmuisl_precedingStep) { + [self qmuisl_updateStepControls]; + if (slider.qmui_stepDidChangeBlock) { slider.qmui_stepDidChangeBlock(slider, slider.qmuisl_precedingStep); } @@ -263,6 +280,14 @@ - (NSUInteger)qmuisl_stepWithValue:(float)value { return step; } +- (void)qmuisl_updateStepControls { + NSInteger step = self.qmui_step; + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.userInteractionEnabled = idx != step;// 让 stepControl 不要影响 thumbView 的事件 + obj.indicator.hidden = idx == step; + }]; +} + - (void)qmuisl_swizzleForStepsIfNeeded { [QMUIHelper executeBlock:^{ OverrideImplementation([UISlider class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { @@ -374,13 +399,7 @@ - (void)qmuisl_layoutStepControls { if (!count) return; // 根据当前 thumbView 的位置,控制重叠的那个 stepControl 的事件响应和显隐,由于 slider 可能是 continuous 的,所以这段逻辑必须每次 layout 都调用,不能放在 layoutCachedKey 的保护里 - CGRect thumbRect = self.qmui_thumbView.frame; CGRect trackRect = [self trackRectForBounds:self.bounds]; - NSUInteger step = round((CGRectGetMidX(thumbRect) - CGRectGetMinX(trackRect)) / CGRectGetWidth(trackRect) * (count - 1)); - [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - obj.userInteractionEnabled = idx != step;// 让 stepControl 不要影响 thumbView 的事件 - obj.indicator.hidden = idx == step; - }]; NSString *layoutCachedKey = [NSString stringWithFormat:@"%.0f-%@", CGRectGetWidth(trackRect), @(count)]; if ([self.qmuisl_layoutCachedKey isEqualToString:layoutCachedKey]) return; diff --git a/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m b/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m index c3cd55dd..09809bfe 100644 --- a/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m @@ -30,7 +30,16 @@ + (UIImageView *)qmui_imageViewInTabBarButton:(UIView *)tabBarButton { if (!tabBarButton) { return nil; } - return [tabBarButton qmui_valueForKey:@"_imageView"]; + if (QMUIHelper.isUsedLiquidGlass) { + for (UIView *subview in tabBarButton.subviews) { + if ([subview isKindOfClass:UIImageView.class]) { + return (UIImageView *)subview; + } + } + return nil; + } else { + return [tabBarButton qmui_valueForKey:@"_imageView"]; + } } @end diff --git a/QMUIKit/UIKitExtensions/UITableView+QMUI.m b/QMUIKit/UIKitExtensions/UITableView+QMUI.m index 7850986a..d13d79a9 100644 --- a/QMUIKit/UIKitExtensions/UITableView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITableView+QMUI.m @@ -21,6 +21,7 @@ #import "QMUILog.h" #import "NSObject+QMUI.h" #import "CALayer+QMUI.h" +#import "QMUICommonDefines.h" const NSUInteger kFloatValuePrecision = 4;// 统一一个小数点运算精度 @@ -238,6 +239,15 @@ - (void)qmui_styledAsQMUITableView { self.qmui_insetGroupedCornerRadius = TableViewInsetGroupedCornerRadius; self.qmui_insetGroupedHorizontalInset = TableViewInsetGroupedHorizontalInset; + +#ifdef IOS26_SDK_ALLOWED + if (@available(iOS 26.0, *)) { + self.topEdgeEffect.hidden = YES; + self.leftEdgeEffect.hidden = YES; + self.bottomEdgeEffect.hidden = YES; + self.rightEdgeEffect.hidden = YES; + } +#endif } - (void)_qmui_configEstimatedRowHeight { diff --git a/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m b/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m index 843ea3de..d64ac93d 100644 --- a/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m @@ -14,6 +14,7 @@ #import "UITraitCollection+QMUI.h" #import "QMUICore.h" +#import "UIApplication+QMUI.h" @implementation UITraitCollection (QMUI) @@ -58,60 +59,50 @@ + (void)_qmui_notifyUserInterfaceStyleWillChangeEvents:(UITraitCollection *)trai } } ++ (void)_qmui_setUserInterfaceStyleForTraitCollection:(UITraitCollection *)traitCollection { + static UIUserInterfaceStyle currentUserInterfaceStyle = UIUserInterfaceStyleUnspecified; + if (currentUserInterfaceStyle == traitCollection.userInterfaceStyle) { + return; + } + currentUserInterfaceStyle = traitCollection.userInterfaceStyle; + + [self _qmui_notifyUserInterfaceStyleWillChangeEvents:traitCollection]; +} + + (void)_qmui_overrideTraitCollectionMethodIfNeeded { [QMUIHelper executeBlock:^{ - static UIUserInterfaceStyle qmui_lastNotifiedUserInterfaceStyle; - qmui_lastNotifiedUserInterfaceStyle = [UITraitCollection currentTraitCollection].userInterfaceStyle; - - // - (void) _willTransitionToTraitCollection:(id)arg1 withTransitionCoordinator:(id)arg2; (0x7fff24711d49) - OverrideImplementation([UIWindow class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"willTransitionToTraitCollection:", @"withTransitionCoordinator:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIWindow *selfObject, UITraitCollection *traitCollection, id coordinator) { - - // call super - void (*originSelectorIMP)(id, SEL, UITraitCollection *, id ); - originSelectorIMP = (void (*)(id, SEL, UITraitCollection *, id ))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, traitCollection, coordinator); - - BOOL snapshotFinishedOnBackground = traitCollection.userInterfaceLevel == UIUserInterfaceLevelElevated && UIApplication.sharedApplication.applicationState == UIApplicationStateBackground; - // 进入后台且完成截图了就不继续去响应 style 变化(实测 iOS 13.0 iPad 进入后台并完成截图后,仍会多次改变 style,但是系统并没有调用界面的相关刷新方法) - if (selfObject.windowScene && !snapshotFinishedOnBackground) { - UIWindow *firstValidatedWindow = nil; + /// https://github.com/Tencent/QMUI_iOS/issues/1634 + if (QMUIHelper.isMac) { + NSString *willChangeTraitCollection = [NSString qmui_stringByConcat:@"_", @"setDefault", @"TraitCollection:", nil]; + OverrideImplementation([UIScreen class], NSSelectorFromString(willChangeTraitCollection), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIScreen *selfObject, UITraitCollection *traitCollection) { - if ([NSStringFromClass(selfObject.class) containsString:@"_UIWindowSceneUserInterfaceStyle"]) { // _UIWindowSceneUserInterfaceStyleAnimationSnapshotWindow - firstValidatedWindow = selfObject; - } else { - // 系统会按照这个数组的顺序去更新 window 的 traitCollection,找出最先响应样式更新的 window - NSPointerArray *windows = [[selfObject windowScene] valueForKeyPath:@"_contextBinder._attachedBindables"]; - for (NSUInteger i = 0, count = windows.count; i < count; i++) { - UIWindow *window = [windows pointerAtIndex:i]; - // 例如用 UIWindow 方式显示的弹窗,在消失后,在 windows 数组里会残留一个 nil 的位置,这里过滤掉,否则会导致 App 从桌面唤醒时无法立即显示正确的 style - if (!window) { - continue;; - } - - // 由于 Keyboard 可以通过 keyboardAppearance 来控制 userInterfaceStyle 的 Dark/Light,不一定和系统一样,这里要过滤掉 - if ([window isKindOfClass:NSClassFromString(@"UIRemoteKeyboardWindow")] || [window isKindOfClass:NSClassFromString(@"UITextEffectsWindow")]) { - continue; - } - if (window.overrideUserInterfaceStyle != UIUserInterfaceStyleUnspecified) { - // 这里需要获取到和系统样式同步的 UserInterfaceStyle(所以指定 overrideUserInterfaceStyle 需要跳过) - // 所以当全部 window.overrideUserInterfaceStyle 都指定为非 UIUserInterfaceStyleUnspecified 时将无法获得当前系统的外观 - continue; - } - firstValidatedWindow = window; - break; - } + if (selfObject == UIScreen.mainScreen) { + [UITraitCollection _qmui_setUserInterfaceStyleForTraitCollection:traitCollection]; } - if (selfObject == firstValidatedWindow) { - if (qmui_lastNotifiedUserInterfaceStyle != traitCollection.userInterfaceStyle) { - qmui_lastNotifiedUserInterfaceStyle = traitCollection.userInterfaceStyle; - [self _qmui_notifyUserInterfaceStyleWillChangeEvents:traitCollection]; - } + // call super + void (*originSelectorIMP)(id, SEL, UITraitCollection *); + originSelectorIMP = (void (*)(id, SEL, UITraitCollection *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, traitCollection); + }; + }); + } else { + NSString *willChangeTraitCollection = [NSString qmui_stringByConcat:@"_", @"parent", @"WillTransitionTo", @"TraitCollection:", nil]; + OverrideImplementation([UIWindow class], NSSelectorFromString(willChangeTraitCollection), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIWindow *selfObject, UITraitCollection *traitCollection) { + + if (selfObject == UIApplication.sharedApplication.qmui_delegateWindow) { + [UITraitCollection _qmui_setUserInterfaceStyleForTraitCollection:traitCollection]; } - } - }; - }); + + // call super + void (*originSelectorIMP)(id, SEL, UITraitCollection *); + originSelectorIMP = (void (*)(id, SEL, UITraitCollection *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, traitCollection); + }; + }); + } } oncePerIdentifier:@"UITraitCollection addUserInterfaceStyleWillChangeObserver"]; } diff --git a/QMUIKit/UIKitExtensions/UIView+QMUI.m b/QMUIKit/UIKitExtensions/UIView+QMUI.m index faf17139..e182e4f3 100644 --- a/QMUIKit/UIKitExtensions/UIView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIView+QMUI.m @@ -45,17 +45,19 @@ + (void)load { }; }); +#ifdef DEBUG // 这个私有方法在 view 被调用 becomeFirstResponder 并且处于 window 上时,才会被调用,所以比 becomeFirstResponder 更适合用来检测 ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], NSSelectorFromString(@"_didChangeToFirstResponder:"), id, ^(UIView *selfObject, id firstArgv) { - if (selfObject == firstArgv && [selfObject conformsToProtocol:@protocol(UITextInput)]) { + if (IS_DEBUG && selfObject == firstArgv && [selfObject conformsToProtocol:@protocol(UITextInput)]) { // 像 QMUIModalPresentationViewController 那种以 window 的形式展示浮层,浮层里的输入框 becomeFirstResponder 的场景,[window makeKeyAndVisible] 被调用后,就会立即走到这里,但此时该 window 尚不是 keyWindow,所以这里延迟到下一个 runloop 里再去判断 dispatch_async(dispatch_get_main_queue(), ^{ - if (IS_DEBUG && ![selfObject isKindOfClass:[UIWindow class]] && selfObject.window && !selfObject.window.keyWindow) { + if (![selfObject isKindOfClass:[UIWindow class]] && selfObject.window && !selfObject.window.isKeyWindow) { [selfObject QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow]; } }); } }); +#endif }); } @@ -310,11 +312,16 @@ - (void)setQmui_viewController:(__kindof UIViewController * _Nullable)qmui_viewC - (__kindof UIViewController *)qmui_viewController { if (self.qmui_isControllerRootView) { - return (__kindof UIViewController *)((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_viewController)).object; + return self.__qmui_associatedViewController; } return self.superview.qmui_viewController; } +- (nullable __kindof UIViewController *)__qmui_associatedViewController { + QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_viewController); + return weakContainer.object; +} + @end @interface UIViewController (QMUI_View) @@ -326,8 +333,15 @@ @implementation UIViewController (QMUI_View) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(setView:), UIView *, ^(UIViewController *selfObject, UIView *view) { + if (view.__qmui_associatedViewController != selfObject) { + view.qmui_viewController = selfObject; + } + }); ExtendImplementationOfVoidMethodWithoutArguments([UIViewController class], @selector(viewDidLoad), ^(UIViewController *selfObject) { - selfObject.view.qmui_viewController = selfObject; + if (selfObject.view.__qmui_associatedViewController != selfObject) { + selfObject.view.qmui_viewController = selfObject; + } }); }); } diff --git a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m index de9fbbcf..2b204356 100644 --- a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m @@ -20,6 +20,7 @@ #import "NSObject+QMUI.h" #import "QMUILog.h" #import "UIView+QMUI.h" +#import "UIApplication+QMUI.h" NSNotificationName const QMUIAppSizeWillChangeNotification = @"QMUIAppSizeWillChangeNotification"; NSString *const QMUIPrecedingAppSizeUserInfoKey = @"QMUIPrecedingAppSizeUserInfoKey"; @@ -62,7 +63,7 @@ + (void)load { OverrideImplementation([UIViewController class], @selector(viewWillTransitionToSize:withTransitionCoordinator:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject, CGSize size, id coordinator) { - if (selfObject == UIApplication.sharedApplication.delegate.window.rootViewController) { + if (selfObject == UIApplication.sharedApplication.qmui_delegateWindow.rootViewController) { CGSize originalSize = selfObject.view.frame.size; BOOL sizeChanged = !CGSizeEqualToSize(originalSize, size); if (sizeChanged) { @@ -587,7 +588,7 @@ - (void)qmui_animateAlongsideTransition:(void (^ __nullable)(id +NS_ASSUME_NONNULL_BEGIN + @interface UIWindow (QMUI) /** @@ -37,5 +39,10 @@ /// 当前 window 因各种原因(例如其他 window 显式调用 makeKey、当前 keyWindow 被隐藏导致系统自动流转 keyWindow、主动向自身调用 resignKeyWindow 等)导致从 keyWindow 转变为非 keyWindow 时会询问这个 block,业务可在这个 block 里干预当前的流转。 /// 实际场景例如,背后 window 正在显示一个带输入框的 webView 网页,输入框聚焦以升起键盘,此时你再新开一个更高 windowLevel 的 window,盖在 webView 上并且 makeKey,就会发现你的 window 依然被键盘挡住,因为 webView 有个特性是如果有输入框聚焦,则 webView 内部会不断地尝试将输入框 becomeFirstResponder 并且让输入框所在的 window makeKey,这就会抢占了我们刚刚手动盖上来的 window 的 key,所以此时就可以给新开的 window 使用本 block,返回 NO,使 webView 无法抢占 keyWindow,从而避免键盘遮挡。 -@property(nonatomic, copy) BOOL (^qmui_canResignKeyWindowBlock)(UIWindow *selfObject, UIWindow *windowWillBecomeKey); +@property(nonatomic, copy, nullable) BOOL (^qmui_canResignKeyWindowBlock)(UIWindow *selfObject, UIWindow *windowWillBecomeKey); + ++ (instancetype)qmui_windowWithWindowScene:(nullable UIWindowScene *)windowScene; + @end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/UIWindow+QMUI.m b/QMUIKit/UIKitExtensions/UIWindow+QMUI.m index 57e03b66..a389ba5a 100644 --- a/QMUIKit/UIKitExtensions/UIWindow+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIWindow+QMUI.m @@ -15,6 +15,7 @@ #import "UIWindow+QMUI.h" #import "QMUICore.h" +#import "UIApplication+QMUI.h" @implementation UIWindow (QMUI) @@ -99,7 +100,7 @@ - (void)qmuiw_hookIfNeeded { } BeginIgnoreDeprecatedWarning - UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; + UIWindow *keyWindow = UIApplication.sharedApplication.qmui_keyWindow; if (result && keyWindow && keyWindow != selfObject && keyWindow.qmui_canResignKeyWindowBlock) { result = keyWindow.qmui_canResignKeyWindowBlock(keyWindow, selfObject); } @@ -128,4 +129,12 @@ - (void)qmuiw_hookIfNeeded { } oncePerIdentifier:@"UIWindow (QMUI) keyWindow"]; } ++ (instancetype)qmui_windowWithWindowScene:(nullable UIWindowScene *)windowScene { + if (windowScene != nil) { + return [[self.class alloc] initWithWindowScene:windowScene]; + } else { + return [[self.class alloc] initWithFrame:UIScreen.mainScreen.bounds]; + } +} + @end