Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.DS_Store
xcuserdata/

.derivedData/

19 changes: 10 additions & 9 deletions QMUIKit/QMUIComponents/QMUIKeyboardManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#import "QMUIKeyboardManager.h"
#import "QMUICore.h"
#import "UIApplication+QMUI.h"
#import "QMUILog.h"
#import "QMUIAppearance.h"
#import "QMUIMultipleDelegates.h"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.delegate.window;
if (mainWindow) {
CGRect keyboardRect = keyboardMoveUserInfo.endFrame;
CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect];
Expand Down Expand Up @@ -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.delegate.window;
if (!mainWindow) {
if (view) {
[view convertRect:rect fromView:nil];
Expand Down Expand Up @@ -855,15 +856,15 @@ + (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];
}].firstObject;

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];
Expand All @@ -885,14 +886,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {

@end

/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.windows`里方便地区分出来
/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.qmui_windows`里方便地区分出来
@interface QMUIModalPresentationWindow : UIWindow

@end
Expand Down
9 changes: 5 additions & 4 deletions QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#import "QMUIModalPresentationViewController.h"
#import "QMUICore.h"
#import "UIApplication+QMUI.h"
#import "UIViewController+QMUI.h"
#import "UIView+QMUI.h"
#import "QMUIKeyboardManager.h"
Expand Down Expand Up @@ -271,7 +272,7 @@ - (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];
Expand Down Expand Up @@ -505,7 +506,7 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
// 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];
self.window.windowLevel = UIWindowLevelQMUIAlertView;
Expand Down Expand Up @@ -751,7 +752,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;
}
Expand All @@ -763,7 +764,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;
}
Expand Down
5 changes: 3 additions & 2 deletions QMUIKit/QMUIComponents/QMUIPopupContainerView.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#import "QMUIPopupContainerView.h"
#import "QMUICore.h"
#import "UIApplication+QMUI.h"
#import "QMUICommonViewController.h"
#import "UIViewController+QMUI.h"
#import "QMUILog.h"
Expand Down Expand Up @@ -697,7 +698,7 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController;
viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations;

self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
self.previousKeyWindow = UIApplication.sharedApplication.qmui_keyWindow;
[self.popupWindow makeKeyAndVisible];

isShowingByWindowMode = YES;
Expand Down Expand Up @@ -798,7 +799,7 @@ - (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) {
if (UIApplication.sharedApplication.qmui_keyWindow == self.popupWindow) {
[self.previousKeyWindow makeKeyWindow];
}

Expand Down
3 changes: 2 additions & 1 deletion QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#import "QMUIThemeManager.h"
#import "QMUICore.h"
#import "UIApplication+QMUI.h"
#import "UIView+QMUITheme.h"
#import "UIViewController+QMUITheme.h"
#import "QMUIThemePrivate.h"
Expand Down Expand Up @@ -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];

Expand Down
5 changes: 5 additions & 0 deletions QMUIKit/QMUICore/QMUICommonDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
#define IOS18_SDK_ALLOWED YES
#endif

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270000
/// 当前编译使用的 Base SDK 版本为 iOS 27.0 及以上
#define IOS27_SDK_ALLOWED YES
#endif

#pragma mark - Clang

#define ArgumentToString(macro) #macro
Expand Down
2 changes: 1 addition & 1 deletion QMUIKit/QMUICore/QMUIConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ - (void)setDefaultStatusBarStyle:(UIStatusBarStyle)defaultStatusBarStyle {
- (NSArray <UIViewController *>*)appearanceUpdatingViewControllersOfClasses:(NSArray<Class<UIAppearanceContainer>> *)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]];
}
Expand Down
20 changes: 19 additions & 1 deletion QMUIKit/UIKitExtensions/NSObject+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,25 @@ @implementation NSObject (QMUI_KeyValueCoding)
- (id)qmui_valueForKey:(NSString *)key {
if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) {
BeginIgnoreUIKVCAccessProhibited
id value = [self valueForKey:key];

id value = nil;
// 修复:增加 KVC 键合法性校验,避免访问未定义的 key 导致崩溃
if ([self respondsToSelector:NSSelectorFromString(key)] ||
[self.class instancesRespondToSelector:NSSelectorFromString(key)] ||
[self.class accessInstanceVariablesDirectly]) {
// 方式1:优先通过 selector 访问(方法/属性)
SEL getterSel = NSSelectorFromString(key);
if ([self respondsToSelector:getterSel]) {
value = ((id (*)(id, SEL))objc_msgSend)(self, getterSel);
} else {
// 方式2:校验实例变量存在性后再通过 KVC 访问
Ivar ivar = class_getInstanceVariable([self class], [key UTF8String]);
if (ivar) {
value = [self valueForKey:key];
}
}
}

EndIgnoreUIKVCAccessProhibited
return value;
}
Expand Down
7 changes: 7 additions & 0 deletions QMUIKit/UIKitExtensions/UIApplication+QMUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ NS_ASSUME_NONNULL_BEGIN

/// 判断当前的 App 是否已经完全启动
@property(nonatomic, assign, readonly) BOOL qmui_didFinishLaunching;

/// 通过 Scene 获取当前 keyWindow,等价于 iOS 27 之前 `UIApplication.keyWindow` 的语义。若找不到则返回 nil。
@property(nullable, nonatomic, readonly) UIWindow *qmui_keyWindow;

/// 通过 Scene 聚合当前 App 的所有 window,等价于 iOS 27 之前 `UIApplication.windows` 的语义。
@property(nonatomic, copy, readonly) NSArray<UIWindow *> *qmui_windows;

@end

NS_ASSUME_NONNULL_END
67 changes: 67 additions & 0 deletions QMUIKit/UIKitExtensions/UIApplication+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@
#import "UIApplication+QMUI.h"
#import "QMUICore.h"

static UIWindow *QMUIKeyWindowInWindowScenes(NSSet<UIScene *> *connectedScenes, NSIndexSet *allowedActivationStates) {
for (UIScene *scene in connectedScenes) {
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
if (allowedActivationStates && ![allowedActivationStates containsIndex:scene.activationState]) {
continue;
}
for (UIWindow *window in ((UIWindowScene *)scene).windows) {
if (window.isKeyWindow) {
return window;
}
}
}
return nil;
}

@implementation UIApplication (QMUI)

QMUISynthesizeBOOLProperty(qmui_didFinishLaunching, setQmui_didFinishLaunching)
Expand Down Expand Up @@ -45,4 +62,54 @@ - (void)qmui_handleDidFinishLaunchingNotification:(NSNotification *)notification
[NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidFinishLaunchingNotification object:nil];
}

- (UIWindow *)qmui_keyWindow {
static NSIndexSet *foregroundActiveStates;
static NSIndexSet *foregroundStates;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
foregroundActiveStates = [NSIndexSet indexSetWithIndex:UISceneActivationStateForegroundActive];
NSMutableIndexSet *states = [NSMutableIndexSet indexSet];
[states addIndex:UISceneActivationStateForegroundInactive];
[states addIndex:UISceneActivationStateForegroundActive];
foregroundStates = states;
});

UIWindow *keyWindow = QMUIKeyWindowInWindowScenes(self.connectedScenes, foregroundActiveStates);
if (!keyWindow) {
keyWindow = QMUIKeyWindowInWindowScenes(self.connectedScenes, foregroundStates);
}
if (!keyWindow) {
keyWindow = QMUIKeyWindowInWindowScenes(self.connectedScenes, nil);
}
#ifndef IOS27_SDK_ALLOWED
if (!keyWindow) {
BeginIgnoreDeprecatedWarning
keyWindow = self.keyWindow;
EndIgnoreDeprecatedWarning
}
#endif
return keyWindow;
}

- (NSArray<UIWindow *> *)qmui_windows {
NSMutableArray<UIWindow *> *windows = [NSMutableArray array];
for (UIScene *scene in self.connectedScenes) {
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
[windows addObjectsFromArray:((UIWindowScene *)scene).windows];
}
if (windows.count > 0) {
return windows;
}
#ifndef IOS27_SDK_ALLOWED
BeginIgnoreDeprecatedWarning
NSArray<UIWindow *> *legacyWindows = self.windows;
EndIgnoreDeprecatedWarning
return legacyWindows ?: @[];
#else
return @[];
#endif
}

@end
5 changes: 3 additions & 2 deletions QMUIKit/UIKitExtensions/UIMenuController+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#import "UIMenuController+QMUI.h"
#import "QMUICore.h"
#import "UIApplication+QMUI.h"
#import "NSArray+QMUI.h"

@implementation UIMenuController (QMUI)
Expand Down Expand Up @@ -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, *)) {
Expand All @@ -136,7 +137,7 @@ + (UIWindow *)qmuimc_menuControllerWindow {

+ (UIWindow *)qmuimc_firstResponderWindowExceptMainWindow {
__block UIWindow *resultWindow = nil;
[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 != UIApplication.sharedApplication.delegate.window) {
UIResponder *responder = [UIMenuController qmuimc_findFirstResponderInView:window];
if (responder) {
Expand Down
19 changes: 19 additions & 0 deletions QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,25 @@ + (void)load {
}

- (UIView *)qmui_contentView {
if (@available(iOS 26.0, *)) {
// 如果 contentView 已经被构造出来,则通过遍历 view 层级树的方式获取
UIView *contentView = [self.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull item) {
return [NSStringFromClass(item.class) containsString:@"ContentView"];
}];
if (contentView) return contentView;

// Xcode 26 编译在 iOS 26 上时,无法用以前的 KVC 方式获取 contentView,所以改为通过 Ivar 获取
NSObject *provider = [self valueForKey:@"visualProvider"];
__block UIView *result = nil;
[provider qmui_enumrateIvarsUsingBlock:^(Ivar _Nonnull ivar, NSString * _Nonnull ivarDescription) {
if (!result && [ivarDescription containsString:@"contentView"]) {
result = getObjectIvarValue(provider, ivar);
}
}];
return result;
}

// 在 bar init 完即可获取到 contentView,虽然此时尚未被添加到 view 上
return [self valueForKeyPath:@"visualProvider.contentView"];
}

Expand Down
2 changes: 1 addition & 1 deletion QMUIKit/UIKitExtensions/UIView+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ + (void)load {
if (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 (IS_DEBUG && ![selfObject isKindOfClass:[UIWindow class]] && selfObject.window && !selfObject.window.isKeyWindow) {
[selfObject QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow];
}
});
Expand Down
Loading