From 368ad6705df7bdf6112821d807c32548cdabde46 Mon Sep 17 00:00:00 2001 From: 0x1306a94 <0x1306a94@gmail.com> Date: Fri, 10 Jun 2022 08:38:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8DXcode14=20beta=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QMUIKit.podspec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/QMUIKit.podspec b/QMUIKit.podspec index 10ab21bd..be28ee35 100644 --- a/QMUIKit.podspec +++ b/QMUIKit.podspec @@ -41,6 +41,11 @@ Pod::Spec.new do |s| s.subspec 'QMUIResources' do |ss| ss.resource_bundles = {'QMUIResources' => ['QMUIKit/QMUIResources/*.*']} + ss.pod_target_xcconfig = { + 'EXPANDED_CODE_SIGN_IDENTITY' => '', + 'CODE_SIGNING_REQUIRED' => 'NO', + 'CODE_SIGNING_ALLOWED' => 'NO', + } end s.subspec 'QMUIWeakObjectContainer' do |ss| From 80f44cfd1740c94478bd613296628d4902a326ca Mon Sep 17 00:00:00 2001 From: molicechen Date: Wed, 10 Aug 2022 06:32:42 +0800 Subject: [PATCH 2/3] 4.5.0 --- QMUIKit.podspec | 7 +- .../UINavigationBar+Transition.m | 49 +++++- ...gationController+NavigationBarTransition.m | 2 + QMUIKit/QMUIComponents/QMUIAlertController.m | 4 +- .../QMUIComponents/QMUIButton/QMUIButton.m | 1 - .../QMUIButton/QMUINavigationButton.m | 1 - .../QMUIComponents/QMUIConsole/QMUIConsole.m | 47 ++++-- .../QMUIConsole/QMUIConsoleViewController.m | 2 +- .../QMUIComponents/QMUIDialogViewController.m | 2 +- .../QMUIImagePreviewView.m | 2 +- QMUIKit/QMUIComponents/QMUIMarqueeLabel.m | 4 - .../NSObject+QMUIMultipleDelegates.m | 44 +++--- .../QMUIMultipleDelegates.m | 4 + .../QMUIComponents/QMUINavigationTitleView.h | 8 +- .../QMUIComponents/QMUINavigationTitleView.m | 106 ++++++++----- .../QMUIPopupMenuButtonItem.h | 2 +- .../QMUIPopupMenuButtonItem.m | 19 ++- .../QMUIPopupMenuItemProtocol.h | 3 +- .../QMUIPopupMenuView/QMUIPopupMenuView.h | 3 + QMUIKit/QMUIComponents/QMUISearchController.m | 11 +- QMUIKit/QMUIComponents/QMUITableView.m | 2 +- QMUIKit/QMUIComponents/QMUITextView.m | 10 +- .../QMUITheme/QMUIThemePrivate.m | 43 ++++-- .../QMUIStaticTableViewCellDataSource.m | 2 +- QMUIKit/QMUICore/QMUICommonDefines.h | 2 +- QMUIKit/QMUICore/QMUIConfiguration.m | 9 +- QMUIKit/QMUICore/QMUIHelper.m | 12 +- QMUIKit/QMUIKit.h | 14 +- .../QMUICommonTableViewController.h | 2 +- .../QMUICommonTableViewController.m | 6 +- .../QMUIMainFrame/QMUINavigationController.m | 9 ++ QMUIKit/UIKitExtensions/CALayer+QMUI.h | 7 + QMUIKit/UIKitExtensions/CALayer+QMUI.m | 9 ++ QMUIKit/UIKitExtensions/NSString+QMUI.m | 4 +- QMUIKit/UIKitExtensions/NSURL+QMUI.m | 2 +- .../QMUIBarProtocol/QMUIBarProtocol.h | 54 +++++++ .../QMUIBarProtocol/QMUIBarProtocolPrivate.h | 28 ++++ .../QMUIBarProtocol/QMUIBarProtocolPrivate.m | 78 ++++++++++ .../UINavigationBar+QMUIBarProtocol.h | 17 ++ .../UINavigationBar+QMUIBarProtocol.m | 124 +++++++++++++++ .../UITabBar+QMUIBarProtocol.h | 17 ++ .../UITabBar+QMUIBarProtocol.m | 124 +++++++++++++++ QMUIKit/UIKitExtensions/QMUIStringPrivate.m | 54 +++++-- .../UIActivityIndicatorView+QMUI.h | 1 + .../UIActivityIndicatorView+QMUI.m | 12 +- QMUIKit/UIKitExtensions/UILabel+QMUI.m | 7 + .../UIKitExtensions/UINavigationBar+QMUI.h | 25 --- .../UIKitExtensions/UINavigationBar+QMUI.m | 98 ++++++------ .../UINavigationController+QMUI.m | 22 ++- QMUIKit/UIKitExtensions/UISlider+QMUI.m | 28 ++-- QMUIKit/UIKitExtensions/UITabBar+QMUI.h | 40 +---- QMUIKit/UIKitExtensions/UITabBar+QMUI.m | 145 +----------------- QMUIKit/UIKitExtensions/UITableView+QMUI.h | 4 +- QMUIKit/UIKitExtensions/UITableView+QMUI.m | 60 ++------ QMUIKit/UIKitExtensions/UITextView+QMUI.m | 30 ++-- QMUIKit/UIKitExtensions/UIToolbar+QMUI.m | 26 ++-- QMUIKit/UIKitExtensions/UIView+QMUI.m | 32 +++- .../UIKitExtensions/UIViewController+QMUI.m | 14 +- QMUIKitTests/UIKitExtensions/NSObjectTests.m | 2 - qmui.xcodeproj/project.pbxproj | 42 ++++- 60 files changed, 1022 insertions(+), 516 deletions(-) create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h create mode 100644 QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m diff --git a/QMUIKit.podspec b/QMUIKit.podspec index be28ee35..4a52c45d 100644 --- a/QMUIKit.podspec +++ b/QMUIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "QMUIKit" - s.version = "4.4.3" + s.version = "4.5.0" s.summary = "致力于提高项目 UI 开发效率的解决方案" s.description = <<-DESC QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 @@ -41,11 +41,6 @@ Pod::Spec.new do |s| s.subspec 'QMUIResources' do |ss| ss.resource_bundles = {'QMUIResources' => ['QMUIKit/QMUIResources/*.*']} - ss.pod_target_xcconfig = { - 'EXPANDED_CODE_SIGN_IDENTITY' => '', - 'CODE_SIGNING_REQUIRED' => 'NO', - 'CODE_SIGNING_ALLOWED' => 'NO', - } end s.subspec 'QMUIWeakObjectContainer' do |ss| diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m index 0b33fa6a..6aed3a52 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m @@ -16,6 +16,7 @@ #import "UINavigationBar+Transition.h" #import "QMUICore.h" #import "UINavigationBar+QMUI.h" +#import "UINavigationBar+QMUIBarProtocol.h" #import "QMUIWeakObjectContainer.h" #import "UIImage+QMUI.h" @@ -53,10 +54,18 @@ + (void)load { } }); - ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), UIImage *, UIBarMetrics, ^(UINavigationBar *selfObject, UIImage *backgroundImage, UIBarMetrics barMetrics) { - if (selfObject.qmuinb_copyStylesToBar) { - [selfObject.qmuinb_copyStylesToBar setBackgroundImage:backgroundImage forBarMetrics:barMetrics]; - } + OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarPosition:barMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIImage *image, UIBarPosition barPosition, UIBarMetrics barMetrics) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, image, barPosition, barMetrics); + + if (selfObject.qmuinb_copyStylesToBar) { + [selfObject.qmuinb_copyStylesToBar setBackgroundImage:image forBarPosition:barPosition barMetrics:barMetrics]; + } + }; }); ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setShadowImage:), UIImage *, ^(UINavigationBar *selfObject, UIImage *firstArgv) { @@ -64,6 +73,34 @@ + (void)load { selfObject.qmuinb_copyStylesToBar.shadowImage = firstArgv; } }); + + OverrideImplementation([UINavigationBar class], @selector(setQmui_effect:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIBlurEffect *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIBlurEffect *); + originSelectorIMP = (void (*)(id, SEL, UIBlurEffect *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.qmui_effect = firstArgv; + } + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(setQmui_effectForegroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIColor *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.qmui_effectForegroundColor = firstArgv; + } + }; + }); }); } @@ -105,9 +142,13 @@ - (void)setQmuinb_copyStylesToBar:(UINavigationBar *)copyStylesToBar { if (![copyStylesToBar.barTintColor isEqual:self.barTintColor]) { copyStylesToBar.barTintColor = self.barTintColor; } + #ifdef IOS15_SDK_ALLOWED } #endif + + copyStylesToBar.qmui_effect = self.qmui_effect; + copyStylesToBar.qmui_effectForegroundColor = self.qmui_effectForegroundColor; } - (UINavigationBar *)qmuinb_copyStylesToBar { diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m index a5aec186..0da926ed 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m @@ -22,6 +22,7 @@ #import "UINavigationBar+Transition.h" #import "QMUINavigationTitleView.h" #import "UINavigationBar+QMUI.h" +#import "UINavigationBar+QMUIBarProtocol.h" #import "UIView+QMUI.h" #import "QMUILog.h" @@ -260,6 +261,7 @@ - (void)layoutTransitionNavigationBar { UIView *backgroundView = self.navigationController.navigationBar.qmui_backgroundView; CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view]; self.transitionNavigationBar.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112 + [self.view bringSubviewToFront:self.transitionNavigationBar];// 避免在后续被其他 subviews 盖住 } } diff --git a/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUIKit/QMUIComponents/QMUIAlertController.m index fde2961d..053466c7 100644 --- a/QMUIKit/QMUIComponents/QMUIAlertController.m +++ b/QMUIKit/QMUIComponents/QMUIAlertController.m @@ -625,7 +625,7 @@ - (void)viewDidLayoutSubviews { self.buttonScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.buttonScrollView.bounds), contentOriginY); // 容器最后布局 CGFloat contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds); - CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds); + CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(SafeAreaInsetsConstantForDeviceWithNotch); if (contentHeight > screenSpaceHeight - 20) { screenSpaceHeight -= 20; CGFloat contentH = fmin(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); @@ -649,7 +649,7 @@ - (void)viewDidLayoutSubviews { self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), contentHeight); self.mainVisualEffectView.frame = self.scrollWrapView.bounds; - self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, (screenSpaceHeight - contentHeight - self.keyboardHeight) / 2, CGRectGetWidth(self.containerView.frame), CGRectGetHeight(self.scrollWrapView.bounds)); + self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + (screenSpaceHeight - contentHeight - self.keyboardHeight) / 2, CGRectGetWidth(self.containerView.frame), CGRectGetHeight(self.scrollWrapView.bounds)); } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { diff --git a/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m b/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m index af307623..61933b34 100644 --- a/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m +++ b/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m @@ -15,7 +15,6 @@ #import "QMUIButton.h" #import "QMUICore.h" -#import "QMUILog.h" #import "CALayer+QMUI.h" #import "UIButton+QMUI.h" diff --git a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m index 83730970..5779769d 100644 --- a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m +++ b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m @@ -19,7 +19,6 @@ #import "UIColor+QMUI.h" #import "UIViewController+QMUI.h" #import "QMUINavigationController.h" -#import "QMUILog.h" #import "UIControl+QMUI.h" #import "UIView+QMUI.h" #import "NSString+QMUI.h" diff --git a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m index c24d27ac..5e7ed03f 100644 --- a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m +++ b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m @@ -21,9 +21,37 @@ #import "UIColor+QMUI.h" #import "QMUITextView.h" +/// 定义一个 class 只是为了在 Lookin 里表达这是一个 console window 而已,不需要实现什么东西 +@interface QMUIConsoleWindow : UIWindow +@end + +@implementation QMUIConsoleWindow + +- (instancetype)init { + if (self = [super init]) { + self.backgroundColor = nil; + if (QMUICMIActivated) { + self.windowLevel = UIWindowLevelQMUIConsole; + } else { + self.windowLevel = 1; + } + self.qmui_capturesStatusBarAppearance = NO; + } + return self; +} + +- (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 + UIView *originalView = [super hitTest:point withEvent:event]; + return originalView == self || originalView == self.rootViewController.view.superview ? nil : originalView; +} + +@end + @interface QMUIConsole () -@property(nonatomic, strong) UIWindow *consoleWindow; +@property(nonatomic, strong) QMUIConsoleWindow *consoleWindow; @property(nonatomic, strong) QMUIConsoleViewController *consoleViewController; @end @@ -65,6 +93,7 @@ + (id)allocWithZone:(struct _NSZone *)zone{ + (void)logWithLevel:(NSString *)level name:(NSString *)name logString:(id)logString { QMUIConsole *console = [QMUIConsole sharedInstance]; + if (!QMUIConsole.sharedInstance.canShow) return; [console initConsoleWindowIfNeeded]; [console.consoleViewController logWithLevel:level name:name logString:logString]; if (console.showConsoleAutomatically) { @@ -105,21 +134,7 @@ + (void)hide { - (void)initConsoleWindowIfNeeded { if (!self.consoleWindow) { - self.consoleWindow = [[UIWindow alloc] init]; - self.consoleWindow.backgroundColor = nil; - if (QMUICMIActivated) { - self.consoleWindow.windowLevel = UIWindowLevelQMUIConsole; - } else { - self.consoleWindow.windowLevel = 1; - } - self.consoleWindow.qmui_capturesStatusBarAppearance = NO; - __weak __typeof(self)weakSelf = self; - self.consoleWindow.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) { - // 当显示 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 - return originalView == weakSelf.consoleWindow || originalView == weakSelf.consoleViewController.view.superview ? nil : originalView; - }; - + self.consoleWindow = [[QMUIConsoleWindow alloc] init]; self.consoleViewController = [[QMUIConsoleViewController alloc] init]; self.consoleWindow.rootViewController = self.consoleViewController; } diff --git a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m index dfd4f15e..3f8c03e0 100644 --- a/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m +++ b/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m @@ -103,7 +103,7 @@ @implementation QMUIConsoleLogItemCell - (void)didInitializeWithStyle:(UITableViewCellStyle)style { [super didInitializeWithStyle:style]; - self.backgroundColor = nil; + self.backgroundColor = UIColor.clearColor; self.selectionStyle = UITableViewCellSelectionStyleNone; self.textView = [[QMUITextView alloc] init]; diff --git a/QMUIKit/QMUIComponents/QMUIDialogViewController.m b/QMUIKit/QMUIComponents/QMUIDialogViewController.m index da408da2..94b2c1dc 100644 --- a/QMUIKit/QMUIComponents/QMUIDialogViewController.m +++ b/QMUIKit/QMUIComponents/QMUIDialogViewController.m @@ -450,8 +450,8 @@ - (void)didInitialize { self.selectedItemIndexes = [[NSMutableSet alloc] init]; self.tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - self.tableView.delegate = self; self.tableView.dataSource = self; + self.tableView.delegate = self; self.tableView.alwaysBounceVertical = NO; self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; diff --git a/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m b/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m index cc98f4a6..9aa70d68 100644 --- a/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m +++ b/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m @@ -83,8 +83,8 @@ - (void)didInitializedWithFrame:(CGRect)frame { self.collectionViewLayout.allowsMultipleItemScroll = NO; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMakeWithSize(frame.size) collectionViewLayout:self.collectionViewLayout]; - self.collectionView.delegate = self; self.collectionView.dataSource = self; + self.collectionView.delegate = self; self.collectionView.backgroundColor = UIColorClear; self.collectionView.showsHorizontalScrollIndicator = NO; self.collectionView.showsVerticalScrollIndicator = NO; diff --git a/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m b/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m index b5e59e7b..c377270e 100644 --- a/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m +++ b/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m @@ -269,10 +269,6 @@ - (BOOL)requestToStopAnimation { @implementation UILabel (QMUI_Marquee) -- (void)dealloc { - [self qmuimq_removeObserver]; -} - - (void)qmui_startNativeMarquee { // 系统有 _startMarqueeIfNecessary、_startMarquee,但直接开启的方法其实是 marqueeRunning BOOL running = YES; diff --git a/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m b/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m index 7bcca098..9b4b9531 100644 --- a/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m +++ b/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m @@ -59,23 +59,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { return; } - // 为这个 selector 创建一个 QMUIMultipleDelegates 容器 NSString *delegateGetterKey = NSStringFromSelector(getter); - if (!self.qmuimd_delegates[delegateGetterKey]) { - objc_property_t prop = class_getProperty(self.class, delegateGetterKey.UTF8String); - QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop]; - if (property.isStrong) { - // strong property - QMUIMultipleDelegates *strongDelegates = [QMUIMultipleDelegates strongDelegates]; - strongDelegates.parentObject = self; - self.qmuimd_delegates[delegateGetterKey] = strongDelegates; - } else { - // weak property - QMUIMultipleDelegates *weakDelegates = [QMUIMultipleDelegates weakDelegates]; - weakDelegates.parentObject = self; - self.qmuimd_delegates[delegateGetterKey] = weakDelegates; - } - } [QMUIHelper executeBlock:^{ IMP originIMP = method_getImplementation(originMethod); @@ -90,16 +74,34 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { return; } + // 为这个 selector 创建一个 QMUIMultipleDelegates 容器 QMUIMultipleDelegates *delegates = selfObject.qmuimd_delegates[delegateGetterKey]; - if (!aDelegate) { // 对应 setDelegate:nil,表示清理所有的 delegate - [delegates removeAllDelegates]; - // 只要 qmui_multipleDelegatesEnabled 开启,就会保证 delegate 一直是 delegates,所以不去调用系统默认的 set nil - // originSelectorIMP(selfObject, originDelegateSetter, nil); + if (delegates) { + [delegates removeAllDelegates]; + [selfObject.qmuimd_delegates removeObjectForKey:delegateGetterKey]; + } + // 必须要清空,否则遇到像 tableView:cellForRowAtIndexPath: 这种“要求返回值不能为 nil” 的场景就会中 assert + // https://github.com/Tencent/QMUI_iOS/issues/1411 + originSelectorIMP(selfObject, originDelegateSetter, nil); return; } + if (!delegates) { + objc_property_t prop = class_getProperty(selfObject.class, delegateGetterKey.UTF8String); + QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop]; + if (property.isStrong) { + // strong property + delegates = [QMUIMultipleDelegates strongDelegates]; + } else { + // weak property + delegates = [QMUIMultipleDelegates weakDelegates]; + } + delegates.parentObject = selfObject; + selfObject.qmuimd_delegates[delegateGetterKey] = delegates; + } + if (aDelegate != delegates) {// 过滤掉容器自身,避免把 delegates 传进去 delegates 里,导致死循环 [delegates addDelegate:aDelegate]; } @@ -125,7 +127,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { } - (void)qmui_removeDelegate:(id)delegate { - if (!self.qmui_multipleDelegatesEnabled) { + if (!self.qmuimd_delegates) { return; } NSMutableArray *delegateGetters = [[NSMutableArray alloc] init]; diff --git a/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m b/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m index 69042b2f..d825d4fd 100644 --- a/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m +++ b/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m @@ -168,6 +168,10 @@ - (BOOL)respondsToSelector:(SEL)aSelector { #pragma mark - Overrides +- (BOOL)isProxy { + return YES; +} + - (BOOL)isKindOfClass:(Class)aClass { BOOL result = [super isKindOfClass:aClass]; if (result) return YES; diff --git a/QMUIKit/QMUIComponents/QMUINavigationTitleView.h b/QMUIKit/QMUIComponents/QMUINavigationTitleView.h index 081aa9b9..72e0d19e 100644 --- a/QMUIKit/QMUIComponents/QMUINavigationTitleView.h +++ b/QMUIKit/QMUIComponents/QMUINavigationTitleView.h @@ -120,7 +120,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { @property(nonatomic, assign) BOOL loadingViewHidden; /* - * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间;如果为NO,loading和title整体居中。默认值为YES。 + * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间(目的是为了让切换 loading 时文字不跳动);如果为NO,loading和title整体居中。默认值为YES。 */ @property(nonatomic, assign) BOOL needsLoadingPlaceholderSpace; @@ -175,3 +175,9 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style; @end + +@interface UIView (QMUINavigationTitleView) + +/// 标记当前 view 是用于自定义的导航栏标题,QMUI 可以帮你自动处理系统的一些布局 bug。对于 QMUINavigationTitleView 而言默认值为 YES,其他 UIView 默认值为 NO +@property(nonatomic, assign) BOOL qmui_useAsNavigationTitleView; +@end diff --git a/QMUIKit/QMUIComponents/QMUINavigationTitleView.m b/QMUIKit/QMUIComponents/QMUINavigationTitleView.m index 7c8265f2..38e3ee4d 100644 --- a/QMUIKit/QMUIComponents/QMUINavigationTitleView.m +++ b/QMUIKit/QMUIComponents/QMUINavigationTitleView.m @@ -48,6 +48,7 @@ - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style { - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect)frame { if (self = [super initWithFrame:frame]) { + self.qmui_useAsNavigationTitleView = YES; [self addTarget:self action:@selector(handleTouchTitleViewEvent) forControlEvents:UIControlEventTouchUpInside]; _contentView = [[UIView alloc] init]; @@ -139,14 +140,17 @@ - (void)updateSubtitleLabelSize { } - (CGSize)loadingViewSpacingSize { - if (self.needsLoadingView) { - return CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height); + if (self.needsLoadingView && (self.needsLoadingPlaceholderSpace || !self.loadingViewHidden)) { + // 意味着希望保持 title 绝对居中,所以不管 loading 是否显示,都固定留空位给 loading + CGSize size = CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height); + return size; } return CGSizeZero; } - (CGSize)loadingViewSpacingSizeIfNeedsPlaceholder { - return CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); + CGSize size = CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); + return size; } - (CGSize)accessorySpacingSize { @@ -276,8 +280,10 @@ - (void)layoutSubviews { firstLineMaxX = firstLineMinX + MIN(firstLineWidth, contentSize.width) - (self.needsLoadingPlaceholderSpace ? [self loadingViewSpacingSize].width : 0); firstLineMinX += self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0; if (self.loadingView) { - self.loadingView.frame = CGRectSetXY(self.loadingView.frame, firstLineMinX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top); - firstLineMinX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) { + self.loadingView.frame = CGRectSetXY(self.loadingView.frame, firstLineMinX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top); + firstLineMinX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + } } if (accessoryView) { accessoryView.frame = CGRectSetXY(accessoryView.frame, firstLineMaxX - CGRectGetWidth(accessoryView.frame), CGFloatGetCenter(self.titleLabelSize.height, CGRectGetHeight(accessoryView.frame)) + titleEdgeInsets.top + self.accessoryViewOffset.y); @@ -319,8 +325,10 @@ - (void)layoutSubviews { CGFloat maxX = maxSize.width - contentOffsetRight - (self.needsLoadingPlaceholderSpace ? loadingViewSpace : 0); if (self.loadingView) { - self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height)); - minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) { + self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height)); + minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + } } if (accessoryView) { accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(maxSize.height, CGRectGetHeight(accessoryView.bounds)) + self.accessoryViewOffset.y); @@ -560,6 +568,14 @@ - (void)setLoadingViewHidden:(BOOL)loadingViewHidden { [self refreshLayout]; } +- (void)setLoadingViewSize:(CGSize)loadingViewSize { + _loadingViewSize = loadingViewSize; + if (self.loadingView) { + self.loadingView.qmui_size = loadingViewSize; + [self refreshLayout]; + } +} + - (void)setActive:(BOOL)active { _active = active; if ([self.delegate respondsToSelector:@selector(didChangedActive:forTitleView:)]) { @@ -626,36 +642,6 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // 修复系统使用自定义 titleView 时的布局问题 - OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationBar *selfObject) { - - QMUINavigationTitleView *titleView = (QMUINavigationTitleView *)selfObject.topItem.titleView; - - if ([titleView isKindOfClass:[QMUINavigationTitleView class]]) { - CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度 - CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)]; - titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动 - - // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值 - if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) { - CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试 - titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height); - } - - // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示 - if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) { - titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width); - } - } - - // call super - void (*originSelectorIMP)(id, SEL); - originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD); - }; - }); - // 让 -[UIViewController setTitle:] 可以自动刷新 QMUINavigationTitle OverrideImplementation([UIViewController class], @selector(setTitle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject, NSString *title) { @@ -803,4 +789,50 @@ - (void)qmui_updateTitleViewToMatchScrollOffsetInViewController:(UIViewControlle } +@end + +@implementation UIView (QMUINavigationTitleView) + +static char kAssociatedObjectKey_useAsNavigationTitleView; +- (void)setQmui_useAsNavigationTitleView:(BOOL)useAsNavigationTitleView { + objc_setAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView, @(useAsNavigationTitleView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (useAsNavigationTitleView) { + [QMUIHelper executeBlock:^{ + // 修复系统使用自定义 titleView 时的布局问题 + OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject) { + + UIView *titleView = selfObject.topItem.titleView; + + if (titleView.qmui_useAsNavigationTitleView) { + CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度 + CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)]; + titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动 + + // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值 + if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) { + CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试 + titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height); + } + + // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示 + if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) { + titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width); + } + } + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + }); + } oncePerIdentifier:@"UIView (QMUINavigationTitleView)"]; + } +} + +- (BOOL)qmui_useAsNavigationTitleView { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView)) boolValue]; +} + @end diff --git a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h index 230362f8..c08b492a 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h +++ b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h @@ -46,7 +46,7 @@ NS_ASSUME_NONNULL_BEGIN @param handler item 点击时的事件回调,需要在这里自行隐藏 aMenuView @return item */ -+ (instancetype)itemWithImage:(nullable UIImage *)image title:(nullable NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *aItem))handler; ++ (instancetype)itemWithImage:(nullable UIImage *)image title:(nullable NSString *)title handler:(nullable void (^)(__kindof QMUIPopupMenuButtonItem *aItem))handler; @end diff --git a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m index f004cb7d..c4af34a2 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m +++ b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m @@ -26,7 +26,7 @@ - (void)updateAppearanceForMenuButtonItem; @implementation QMUIPopupMenuButtonItem -+ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *))handler { ++ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(nullable void (^)(__kindof QMUIPopupMenuButtonItem *))handler { QMUIPopupMenuButtonItem *item = [[self alloc] init]; item.image = image; item.title = title; @@ -87,6 +87,23 @@ - (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { } - (void)handleButtonEvent:(id)sender { + if (self.menuView.willHandleButtonItemEventBlock) { + BOOL found = NO; + for (NSInteger section = 0, sectionCount = self.menuView.itemSections.count; section < sectionCount; section ++) { + NSArray *items = self.menuView.itemSections[section]; + for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) { + QMUIPopupMenuBaseItem *item = items[row]; + if (item == self) { + self.menuView.willHandleButtonItemEventBlock(self.menuView, self, section, row); + found = YES; + break; + } + } + if (found) { + break; + } + } + } if (self.handler) { self.handler(self); } diff --git a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h index 9338c5db..44431c57 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h +++ b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h @@ -27,7 +27,8 @@ NS_ASSUME_NONNULL_BEGIN /// item 的高度,默认为 -1,-1 表示高度以 QMUIPopupMenuView.itemHeight 为准。如果设置为 QMUIViewSelfSizingHeight,则表示高度由 -[self sizeThatFits:] 返回的值决定。 @property(nonatomic, assign) CGFloat height; -/// item 被点击时的事件处理,需要在内部自行隐藏 QMUIPopupMenuView。 +/// item 被点击时的事件处理接口,QMUIPopupMenuBaseItem 里仅声明,只有 QMUIPopupMenuButtonItem 会自动调用。若继承 QMUIPopupMenuBaseItem 衍生自己的子类,也需要手动调用它。 +/// @note 需要在内部自行隐藏 QMUIPopupMenuView。 @property(nonatomic, copy, nullable) void (^handler)(__kindof NSObject *aItem); /// 当前 item 所在的 QMUIPopupMenuView 的引用,只有在 item 被添加到菜单之后才有值。 diff --git a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h index f52d7cbb..706b4024 100644 --- a/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h +++ b/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h @@ -68,6 +68,9 @@ NS_ASSUME_NONNULL_BEGIN /// 批量设置 item 的样式 @property(nonatomic, copy, nullable) void (^itemConfigurationHandler)(QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuBaseItem *aItem, NSInteger section, NSInteger index); +/// 如果 items 是 QMUIPopupMenuButtonItem 或其子类,则当任一 item 被点击前,都会先调用这个 block。 +@property(nonatomic, copy, nullable) void (^willHandleButtonItemEventBlock)(QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index); + /// 设置 item,均处于同一个 section 内 @property(nonatomic, copy, nullable) NSArray<__kindof QMUIPopupMenuBaseItem *> *items; diff --git a/QMUIKit/QMUIComponents/QMUISearchController.m b/QMUIKit/QMUIComponents/QMUISearchController.m index 3cf9809d..e35e53f7 100644 --- a/QMUIKit/QMUIComponents/QMUISearchController.m +++ b/QMUIKit/QMUIComponents/QMUISearchController.m @@ -70,6 +70,15 @@ @interface QMUICustomSearchController : UISearchController @implementation QMUICustomSearchController +- (instancetype)initWithSearchResultsController:(UIViewController *)searchResultsController { + if (self = [super initWithSearchResultsController:searchResultsController]) { + if (@available(iOS 15.0, *)) { + self.dimsBackgroundDuringPresentation = YES;// iOS 15 开始该默认值为 NO 了,为了保持与旧版本一致的表现,这里改默认值 + } + } + return self; +} + - (void)setCustomDimmingView:(UIView *)customDimmingView { if (_customDimmingView != customDimmingView) { [_customDimmingView removeFromSuperview]; @@ -391,7 +400,7 @@ - (BOOL)shouldShowSearchBar { - (void)initSearchController { if ([self isViewLoaded] && self.shouldShowSearchBar && !self.searchController) { - self.searchController = [[QMUISearchController alloc] initWithContentsViewController:self]; + self.searchController = [[QMUISearchController alloc] initWithContentsViewController:self resultsTableViewStyle:self.tableView.qmui_style]; self.searchController.searchResultsDelegate = self; self.searchController.searchBar.placeholder = @"搜索"; self.searchController.searchBar.qmui_usedAsTableHeaderView = YES;// 以 tableHeaderView 的方式使用 searchBar 的话,将其置为 YES,以辅助兼容一些系统 bug diff --git a/QMUIKit/QMUIComponents/QMUITableView.m b/QMUIKit/QMUIComponents/QMUITableView.m index a24f80d0..f65a5c16 100644 --- a/QMUIKit/QMUIComponents/QMUITableView.m +++ b/QMUIKit/QMUIComponents/QMUITableView.m @@ -41,8 +41,8 @@ - (void)didInitialize { } - (void)dealloc { - self.delegate = nil; self.dataSource = nil; + self.delegate = nil; } // 保证一直存在tableFooterView,以去掉列表内容不满一屏时尾部的空白分割线 diff --git a/QMUIKit/QMUIComponents/QMUITextView.m b/QMUIKit/QMUIComponents/QMUITextView.m index 224e70cb..a0f6b704 100644 --- a/QMUIKit/QMUIComponents/QMUITextView.m +++ b/QMUIKit/QMUIComponents/QMUITextView.m @@ -281,7 +281,7 @@ - (void)handleTextChanged:(id)sender { // 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ textView.shouldRejectSystemScroll = NO; - [textView qmui_scrollCaretVisibleAnimated:NO]; + [textView qmui_scrollCaretVisibleAnimated:YES]; }); } @@ -315,7 +315,15 @@ - (void)setFrame:(CGRect)frame { // https://github.com/Tencent/QMUI_iOS/issues/557 frame = CGRectFlatted(frame); + // 系统的 UITextView 只要调用 setFrame: 不管 rect 有没有变化都会触发 setContentOffset,引起最后一行输入过程中文字抖动的问题,所以这里屏蔽掉 + BOOL sizeChanged = !CGSizeEqualToSize(frame.size, self.frame.size); + if (!sizeChanged) { + self.shouldRejectSystemScroll = YES; + } [super setFrame:frame]; + if (!sizeChanged) { + self.shouldRejectSystemScroll = NO; + } } - (void)setBounds:(CGRect)bounds { diff --git a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m index 0b98c824..9ce67d62 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m +++ b/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m @@ -78,7 +78,19 @@ + (void)load { NSStringFromSelector(@selector(separatorColor)),], NSStringFromClass(UITableViewCell.class): @[NSStringFromSelector(@selector(qmui_selectedBackgroundColor)),], NSStringFromClass(UICollectionViewCell.class): @[NSStringFromSelector(@selector(qmui_selectedBackgroundColor)),], - NSStringFromClass(UINavigationBar.class): @[NSStringFromSelector(@selector(barTintColor)),], + NSStringFromClass(UINavigationBar.class): ({ + NSMutableArray *result = @[ + NSStringFromSelector(@selector(qmui_effect)), + NSStringFromSelector(@selector(qmui_effectForegroundColor)), + ].mutableCopy; + if (@available(iOS 15.0, *)) { + // iOS 15 在 UINavigationBar (QMUI) 里对所有旧版接口都映射到 standardAppearance,所以重新设置一次 standardAppearance 就可以更新所有样式 + [result addObject:NSStringFromSelector(@selector(standardAppearance))]; + } else { + [result addObjectsFromArray:@[NSStringFromSelector(@selector(barTintColor)),]]; + } + result.copy; + }), NSStringFromClass(UIToolbar.class): @[NSStringFromSelector(@selector(barTintColor)),], NSStringFromClass(UITabBar.class): ({ NSMutableArray *result = @[ @@ -167,21 +179,20 @@ @implementation UIView (QMUIThemeCompatibility) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - if (@available(iOS 13.0, *)) { - } else { - OverrideImplementation([UIView class], @selector(setTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIColor *tintColor) { - - // iOS 12 及以下,-[UIView setTintColor:] 被调用时,如果参数的 tintColor 与当前的 tintColor 指针相同,则不会触发 tintColorDidChange,但这对于 dynamic color 而言是不满足需求的(同一个 dynamic color 实例在任何时候返回的 rawColor 都有可能发生变化),所以这里主动为其做一次 copy 操作,规避指针地址判断的问题 - if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.tintColor) tintColor = tintColor.copy; - - // call super - void (*originSelectorIMP)(id, SEL, UIColor *); - originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, tintColor); - }; - }); - } + // iOS 12 及以下,-[UIView setTintColor:] 被调用时,如果参数的 tintColor 与当前的 tintColor 指针相同,则不会触发 tintColorDidChange,但这对于 dynamic color 而言是不满足需求的(同一个 dynamic color 实例在任何时候返回的 rawColor 都有可能发生变化),所以这里主动为其做一次 copy 操作,规避指针地址判断的问题 + // 2022-7-20 后来发现 iOS 13-15,UIImageView、UIButton,手动切换 theme 时,tintColor 不 copy 就无法刷新,但如果是系统 Dark Mode 切换引发的 setTintColor:,即便不用 copy 也可以刷新,所以这里统一对所有 iOS 版本都做一次 copy + // https://github.com/Tencent/QMUI_iOS/issues/1418 + OverrideImplementation([UIView class], @selector(setTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.tintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); // iOS 12 及以下的版本,[UIView setBackgroundColor:] 并不会保存传进来的 color,所以要自己用个变量保存起来,不然 QMUIThemeColor 对象就会被丢弃 if (@available(iOS 13.0, *)) { diff --git a/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m b/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m index 839ff0e0..ccb14970 100644 --- a/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m +++ b/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m @@ -54,8 +54,8 @@ - (void)setCellDataSections:(NSArray *> * - (void)setTableView:(UITableView *)tableView { _tableView = tableView; // 触发 UITableView (QMUI_StaticCell) 里重写的 setter 里的逻辑 - tableView.delegate = tableView.delegate; tableView.dataSource = tableView.dataSource; + tableView.delegate = tableView.delegate; } @end diff --git a/QMUIKit/QMUICore/QMUICommonDefines.h b/QMUIKit/QMUICore/QMUICommonDefines.h index 4f0a71e3..6abdf457 100644 --- a/QMUIKit/QMUICore/QMUICommonDefines.h +++ b/QMUIKit/QMUICore/QMUICommonDefines.h @@ -181,7 +181,7 @@ #define StatusBarHeight (UIApplication.sharedApplication.statusBarHidden ? 0 : UIApplication.sharedApplication.statusBarFrame.size.height) /// 状态栏高度(如果状态栏不可见,也会返回一个普通状态下可见的高度) -#define StatusBarHeightConstant (UIApplication.sharedApplication.statusBarHidden ? (IS_IPAD ? (IS_NOTCHED_SCREEN ? 24 : 20) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 0 : ([[QMUIHelper deviceModel] isEqualToString:@"iPhone12,1"] ? 48 : (IS_61INCH_SCREEN_AND_IPHONE12 || IS_67INCH_SCREEN ? 47 : 44)), 20)) : UIApplication.sharedApplication.statusBarFrame.size.height) +#define StatusBarHeightConstant (UIApplication.sharedApplication.statusBarHidden ? (IS_IPAD ? (IS_NOTCHED_SCREEN ? 24 : 20) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 0 : ([[QMUIHelper deviceModel] isEqualToString:@"iPhone12,1"] ? 48 : (IS_61INCH_SCREEN_AND_IPHONE12 || IS_67INCH_SCREEN ? 47 : ((IS_54INCH_SCREEN && IOS_VERSION >= 15.0) ? 50 : 44))), 20)) : UIApplication.sharedApplication.statusBarFrame.size.height) /// navigationBar 的静态高度 #define NavigationBarHeight (IS_IPAD ? (IOS_VERSION >= 12.0 ? 50 : 44) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44)) diff --git a/QMUIKit/QMUICore/QMUIConfiguration.m b/QMUIKit/QMUICore/QMUIConfiguration.m index 17ac0abc..80134a92 100644 --- a/QMUIKit/QMUICore/QMUIConfiguration.m +++ b/QMUIKit/QMUICore/QMUIConfiguration.m @@ -40,10 +40,15 @@ @implementation UIViewController (QMUIConfiguration) } if ([self isKindOfClass:UINavigationController.class]) { [viewControllers addObjectsFromArray:[((UINavigationController *)self).visibleViewController qmui_existingViewControllersOfClasses:classes]]; - } - if ([self isKindOfClass:UITabBarController.class]) { + } else if ([self isKindOfClass:UITabBarController.class]) { [viewControllers addObjectsFromArray:[((UITabBarController *)self).selectedViewController qmui_existingViewControllersOfClasses:classes]]; + } else { + // 如果不是常见的 container viewController,则直接获取所有 childViewController + for (UIViewController *child in self.childViewControllers) { + [viewControllers addObjectsFromArray:[child qmui_existingViewControllersOfClasses:classes]]; + } } + for (Class class in classes) { if ([self isKindOfClass:class]) { [viewControllers addObject:self]; diff --git a/QMUIKit/QMUICore/QMUIHelper.m b/QMUIKit/QMUICore/QMUIHelper.m index 6bea87ad..4b3aa45b 100644 --- a/QMUIKit/QMUICore/QMUIHelper.m +++ b/QMUIKit/QMUICore/QMUIHelper.m @@ -852,7 +852,17 @@ + (CGSize)applicationSize { BeginIgnoreDeprecatedWarning CGRect applicationFrame = [UIScreen mainScreen].applicationFrame; EndIgnoreDeprecatedWarning - return CGSizeMake(applicationFrame.size.width + applicationFrame.origin.x, applicationFrame.size.height + applicationFrame.origin.y); + 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; + if (window) { + applicationSize = window.bounds.size; + } else { + applicationSize = UIWindow.new.bounds.size; + } + } + return applicationSize; } @end diff --git a/QMUIKit/QMUIKit.h b/QMUIKit/QMUIKit.h index 236fb36b..dd7fa8a5 100644 --- a/QMUIKit/QMUIKit.h +++ b/QMUIKit/QMUIKit.h @@ -13,7 +13,7 @@ #ifndef QMUIKit_h #define QMUIKit_h -static NSString * const QMUI_VERSION = @"4.4.3"; +static NSString * const QMUI_VERSION = @"4.5.0"; #if __has_include("CAAnimation+QMUI.h") #import "CAAnimation+QMUI.h" @@ -103,6 +103,10 @@ static NSString * const QMUI_VERSION = @"4.4.3"; #import "QMUIBadgeProtocol.h" #endif +#if __has_include("QMUIBarProtocol.h") +#import "QMUIBarProtocol.h" +#endif + #if __has_include("QMUIButton.h") #import "QMUIButton.h" #endif @@ -523,6 +527,10 @@ static NSString * const QMUI_VERSION = @"4.4.3"; #import "UINavigationBar+QMUI.h" #endif +#if __has_include("UINavigationBar+QMUIBarProtocol.h") +#import "UINavigationBar+QMUIBarProtocol.h" +#endif + #if __has_include("UINavigationController+NavigationBarTransition.h") #import "UINavigationController+NavigationBarTransition.h" #endif @@ -559,6 +567,10 @@ static NSString * const QMUI_VERSION = @"4.4.3"; #import "UITabBar+QMUI.h" #endif +#if __has_include("UITabBar+QMUIBarProtocol.h") +#import "UITabBar+QMUIBarProtocol.h" +#endif + #if __has_include("UITabBarItem+QMUI.h") #import "UITabBarItem+QMUI.h" #endif diff --git a/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h b/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h index 1eeb00ca..e3c6a6f8 100644 --- a/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h +++ b/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h @@ -71,8 +71,8 @@ extern NSString *const QMUICommonTableViewControllerSectionFooterIdentifier; * @example * - (void)initTableView { * self.tableView = [MyTableView alloc] initWithFrame:self.view.bounds style:self.style]; - * self.tableView.delegate = self; * self.tableView.dataSource = self; + * self.tableView.delegate = self; * } */ - (void)initTableView; diff --git a/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m b/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m index 8a06c95e..e5481da6 100644 --- a/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m +++ b/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m @@ -64,8 +64,8 @@ - (void)didInitializeWithStyle:(UITableViewStyle)style { - (void)dealloc { // 用下划线而不是self.xxx来访问tableView,避免dealloc时self.view尚未被加载,此时调用self.tableView反而会触发loadView - _tableView.delegate = nil; _tableView.dataSource = nil; + _tableView.delegate = nil; } - (NSString *)description { @@ -302,8 +302,10 @@ @implementation QMUICommonTableViewController (QMUISubclassingHooks) - (void)initTableView { if (!_tableView) { self.tableView = [[QMUITableView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero style:self.style]; - _tableView.delegate = self; + // setDataSource: 不会触发 tableView reload,而 setDelegate: 可以,所以把 setDelegate: 放在后面,保证 reload 时能访问到 dataSource 里的数据源。 + // 否则如果列表开启了 estimated,然后在 viewDidLoad 里设置 tableHeaderView,则 setTableHeaderView: 时由于 setDataSource: 后 tableView 其实没再刷新过,所以内部依然认为 numberOfSections 是默认的1,于是就会去调用 numberOfRows,如果此时 numberOfRows 里用 indexPath 作为下标去访问数据源就会产生越界(因为此时数据源可能还是空的) _tableView.dataSource = self; + _tableView.delegate = self; } } diff --git a/QMUIKit/QMUIMainFrame/QMUINavigationController.m b/QMUIKit/QMUIMainFrame/QMUINavigationController.m index b419566b..2e77a4df 100644 --- a/QMUIKit/QMUIMainFrame/QMUINavigationController.m +++ b/QMUIKit/QMUIMainFrame/QMUINavigationController.m @@ -25,6 +25,7 @@ #import "QMUILog.h" #import "QMUIMultipleDelegates.h" #import "QMUIWeakObjectContainer.h" +#import @protocol QMUI_viewWillAppearNotifyDelegate @@ -456,6 +457,14 @@ - (UIViewController *)childViewControllerForStatusBarWithCustomBlock:(BOOL (^)(U // 1. 有 modal present 则优先交给 modal present 的 vc 控制(例如进入搜索状态且没指定 definesPresentationContext 的 UISearchController) UIViewController *childViewController = self.visibleViewController; + // 修复在 root controller 实现了 preferredStatusBarStyle 方法并且在其中调用 childViewControllerForStatusBarStyle 方法的情况下,iOS 12 present 起 AVPlayerViewController 在 dismiss 时会触发 preferredStatusBarStyle 死循环的 bug:因为 AVPlayerViewController 内部的 preferredStatusBarStyle 会转向 presentingViewController 的 preferredStatusBarStyle,而后者又会 return AVPlayerViewController,于是死循环 + if (@available(iOS 13.0, *)) { + } else { + if ([childViewController isKindOfClass:AVPlayerViewController.class]) { + return nil; + } + } + // 2. 如果 modal present 是一个 UINavigationController,则 self.visibleViewController 拿到的是该 UINavigationController.topViewController,而不是该 UINavigationController 本身,所以这里要特殊处理一下,才能让下文的 beingDismissed 判断生效 if (childViewController.navigationController && (self.presentedViewController == childViewController.navigationController)) { childViewController = childViewController.navigationController; diff --git a/QMUIKit/UIKitExtensions/CALayer+QMUI.h b/QMUIKit/UIKitExtensions/CALayer+QMUI.h index bd8e1df8..de92afb6 100644 --- a/QMUIKit/UIKitExtensions/CALayer+QMUI.h +++ b/QMUIKit/UIKitExtensions/CALayer+QMUI.h @@ -17,6 +17,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { QMUILayerMinXMinYCorner = 1U << 0, QMUILayerMaxXMinYCorner = 1U << 1, @@ -44,6 +46,9 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { /// iOS11 以下 layer 自身的 cornerRadius 一直都是 0,圆角的是通过 mask 做的,qmui_originCornerRadius 保存了当前的圆角 @property(nonatomic, assign, readonly) CGFloat qmui_originCornerRadius; +/// 获取指定 name 值的 layer,包括 self 和 self.sublayers,会一直往 sublayers 查找直到找到目标 layer。 +- (nullable __kindof CALayer *)qmui_layerWithName:(NSString *)name; + /** * 把某个 sublayer 移动到当前所有 sublayers 的最后面 * @param sublayer 要被移动的 layer @@ -114,3 +119,5 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { /// iOS 13 系统设置里的界面样式变化(Dark Mode),以及 QMUIThemeManager 触发的主题变化,都会自动调用 layer 的这个方法,业务无需关心。 - (void)qmui_setNeedsUpdateDynamicStyle NS_REQUIRES_SUPER; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/CALayer+QMUI.m b/QMUIKit/UIKitExtensions/CALayer+QMUI.m index 7996c33e..8fef751d 100644 --- a/QMUIKit/UIKitExtensions/CALayer+QMUI.m +++ b/QMUIKit/UIKitExtensions/CALayer+QMUI.m @@ -145,6 +145,15 @@ - (QMUICornerMask)qmui_maskedCorners { return [objc_getAssociatedObject(self, &kAssociatedObjectKey_maskedCorners) unsignedIntegerValue]; } +- (__kindof CALayer *)qmui_layerWithName:(NSString *)name { + if ([self.name isEqualToString:name]) return self; + for (CALayer *sublayer in self.sublayers) { + CALayer *result = [sublayer qmui_layerWithName:name]; + if (result) return result; + } + return nil; +} + - (void)qmui_sendSublayerToBack:(CALayer *)sublayer { [self insertSublayer:sublayer atIndex:0]; } diff --git a/QMUIKit/UIKitExtensions/NSString+QMUI.m b/QMUIKit/UIKitExtensions/NSString+QMUI.m index 128ba268..42ccc633 100644 --- a/QMUIKit/UIKitExtensions/NSString+QMUI.m +++ b/QMUIKit/UIKitExtensions/NSString+QMUI.m @@ -42,7 +42,9 @@ @implementation NSString (QMUI) } - (NSString *)qmui_trim { - return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSMutableCharacterSet * characterSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; + [characterSet addCharactersInString:@"\0"]; + return [self stringByTrimmingCharactersInSet:characterSet]; } - (NSString *)qmui_trimAllWhiteSpace { diff --git a/QMUIKit/UIKitExtensions/NSURL+QMUI.m b/QMUIKit/UIKitExtensions/NSURL+QMUI.m index d57a0faf..811d6fd2 100644 --- a/QMUIKit/UIKitExtensions/NSURL+QMUI.m +++ b/QMUIKit/UIKitExtensions/NSURL+QMUI.m @@ -27,7 +27,7 @@ @implementation NSURL (QMUI) [urlComponents.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (obj.name) { - [params setObject:obj.value ?: [NSNull null] forKey:obj.name]; + [params setObject:obj.value ?: @"" forKey:obj.name]; } }]; return [params copy]; diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h new file mode 100644 index 00000000..ec4ac289 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h @@ -0,0 +1,54 @@ +// +// QMUIBarProtocol.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + UINavigationBar、UITabBar 在一些特性上基本相同,但它们又是分别继承自 UIView 的,导致很多属性、方法都需要两边添加,所以这里建了个协议,分别在 UINavigationBar、UITabBar 里实现,以保证两边的功能是相同的。 + */ +@protocol QMUIBarProtocol + +/** + bar 的背景 view,可能显示磨砂、背景图。 + 在 iOS 10 及以后是私有的 _UIBarBackground 类。 + 在 iOS 9 及以前是私有的类,对 UINavigationBar 来说是 _UINavigationBarBackground,对 UITabBar 来说是 _UITabBarBackgroundView。 + */ +@property(nullable, nonatomic, strong, readonly) UIView *qmui_backgroundView; + +/** + qmui_backgroundView 内的 subview,用于显示分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 + */ +@property(nullable, nonatomic, strong, readonly) UIImageView *qmui_shadowImageView; + +/** + 获取 bar 里面的磨砂背景,具体的 view 层级是 UIBar → _UIBarBackground → UIVisualEffectView。仅在 bar 的样式确定之后系统才会创建。 + iOS 15 及以后,bar 里可能会同时存在多个磨砂背景(详见 @c qmui_effectViews ),这个属性会获取其中正在显示的那个磨砂,如果两个都在显示,则取 view 层级树里更上层的那个。 + */ +@property(nullable, nonatomic, strong, readonly) UIVisualEffectView *qmui_effectView; + +/** + iOS 15 及以后,由于 bar 的样式在滚动到顶部和底部会有不同,所以可能同时存在两个 effectView。 + */ +@property(nullable, nonatomic, strong, readonly) NSArray *qmui_effectViews; + +/** + 允许直接指定 tab 具体的磨砂样式(系统的仅在 iOS 13 及以后用 UINavigation(Tab)BarAppearance.backgroundEffects 才可以实现)。默认为 nil,如果你没设置过这个属性,那么 nil 的行为就是维持系统的样式,但如果你主动设置过这个属性,那么后续的 nil 则表示把磨砂清空(也即可能出现背景透明的 bar)。 + @note 生效的前提是 backgroundImage、barTintColor 都为空,因为这两者的优先级都比磨砂高。 + */ +@property(nullable, nonatomic, strong) UIBlurEffect *qmui_effect; + +/** + 当 tabBar 展示磨砂的样式时,可以通过这个属性精准指定磨砂的前景色(可参考 CALayer(QMUI).qmui_foregroundColor),因为系统的某些 UIBlurEffectStyle 会自带前景色,且不可去掉,那种情况下你就无法得到准确的自定义前景色了(即便你试图通过设置半透明的 barTintColor 来达到前景色的效果,那也依然会叠加一层系统自带的半透明前景色)。 + */ +@property(nullable, nonatomic, strong) UIColor *qmui_effectForegroundColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h new file mode 100644 index 00000000..e9377b49 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h @@ -0,0 +1,28 @@ +// +// QMUIBarProtocolPrivate.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol QMUIBarProtocolPrivate + +@required +@property(nonatomic, assign) BOOL qmuibar_hasSetEffect; +@property(nonatomic, assign) BOOL qmuibar_hasSetEffectForegroundColor; +@property(nonatomic, strong, readonly, nullable) NSArray *qmuibar_backgroundEffects; +- (void)qmuibar_updateEffect; +@end + +@interface QMUIBarProtocolPrivate : NSObject + ++ (void)swizzleBarBackgroundViewIfNeeded; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m new file mode 100644 index 00000000..02be4343 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m @@ -0,0 +1,78 @@ +// +// QMUIBarProtocolPrivate.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import "QMUIBarProtocolPrivate.h" +#import "QMUIBarProtocol.h" +#import "QMUICore.h" + +@implementation QMUIBarProtocolPrivate + ++ (void)swizzleBarBackgroundViewIfNeeded { + [QMUIHelper executeBlock:^{ + Class backgroundClass = NSClassFromString(@"_UIBarBackground"); + + OverrideImplementation(backgroundClass, @selector(didMoveToSuperview), ^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); + + if ([selfObject.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview; + if (bar.qmuibar_hasSetEffect || bar.qmuibar_hasSetEffectForegroundColor) { + [bar qmuibar_updateEffect]; + } + } + }; + }); + + OverrideImplementation(backgroundClass, @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIView *subview) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, subview); + + // 注意可能存在多个 UIVisualEffectView,例如用于 shadowImage 的 _UIBarBackgroundShadowView,需要过滤掉 + if ([subview isMemberOfClass:UIVisualEffectView.class] + && [selfObject.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview; + if (bar.qmuibar_hasSetEffect || bar.qmuibar_hasSetEffectForegroundColor) { + [bar qmuibar_updateEffect]; + } + } + }; + }); + + // 系统会在任意可能的时机去刷新 backgroundEffects,为了避免被系统的值覆盖,这里需要重写它 + OverrideImplementation(UIVisualEffectView.class, NSSelectorFromString(@"setBackgroundEffects:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIVisualEffectView *selfObject, NSArray *firstArgv) { + + if ([selfObject.superview isKindOfClass:backgroundClass] + && [selfObject.superview.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview.superview; + if (bar.qmui_effectView == selfObject) { + if (bar.qmuibar_hasSetEffect) { + firstArgv = bar.qmuibar_backgroundEffects; + } + } + } + + // call super + void (*originSelectorIMP)(id, SEL, NSArray *); + originSelectorIMP = (void (*)(id, SEL, NSArray *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + } oncePerIdentifier:@"QMUIBarProtocolPrivate"]; +} + +@end diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h new file mode 100644 index 00000000..2a722527 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h @@ -0,0 +1,17 @@ +// +// UINavigationBar+QMUIBarProtocol.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import +#import "QMUIBarProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationBar (QMUIBarProtocol) +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m new file mode 100644 index 00000000..d67c06b8 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m @@ -0,0 +1,124 @@ +// +// UINavigationBar+QMUIBarProtocol.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import "UINavigationBar+QMUIBarProtocol.h" +#import "QMUIBarProtocolPrivate.h" +#import "QMUICore.h" +#import "UIVisualEffectView+QMUI.h" +#import "NSArray+QMUI.h" + +@interface UINavigationBar () +@end + +@implementation UINavigationBar (QMUIBarProtocol) + +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffect, setQmuibar_hasSetEffect) +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffectForegroundColor, setQmuibar_hasSetEffectForegroundColor) + +BeginIgnoreClangWarning(-Wobjc-protocol-method-implementation) +- (void)qmuibar_updateEffect { + [self.qmui_effectViews enumerateObjectsUsingBlock:^(UIVisualEffectView * _Nonnull effectView, NSUInteger idx, BOOL * _Nonnull stop) { + if (self.qmuibar_hasSetEffect) { + // 这里对 iOS 13 不使用 UITabBarAppearance.backgroundEffect 来修改,是因为反正不管 iOS 10 还是 13,最终都是 setBackgroundEffects: 在起作用,而且不用 UITabBarAppearance 还可以规避与 UIAppearance 机制的冲突 + NSArray *effects = self.qmuibar_backgroundEffects; + [effectView qmui_performSelector:NSSelectorFromString(@"setBackgroundEffects:") withArguments:&effects, nil]; + } + if (self.qmuibar_hasSetEffectForegroundColor) { + effectView.qmui_foregroundColor = self.qmui_effectForegroundColor; + } + }]; +} +EndIgnoreClangWarning + +// UITabBar、UIVisualEffectView 都有一个私有的方法 backgroundEffects,当 UIVisualEffectView 应用于 UITabBar 场景时,磨砂的效果实际上被放在 backgroundEffects 内,而不是公开接口的 effect 属性里,这里为了方便,将 UITabBar (QMUI).effect 转成可用于 backgroundEffects 的数组 +- (NSArray *)qmuibar_backgroundEffects { + if (self.qmuibar_hasSetEffect) { + return self.qmui_effect ? @[self.qmui_effect] : nil; + } + return nil; +} + +#pragma mark - + +- (UIView *)qmui_backgroundView { + return [self qmui_valueForKey:@"_backgroundView"]; +} + +- (UIImageView *)qmui_shadowImageView { + // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 + if (@available(iOS 13, *)) { + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; + } + // iOS 10 及以后,在 bar 初始化之后就能获取到 backgroundView 和 shadowView 了 + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"]; +} + +- (UIVisualEffectView *)qmui_effectView { + NSArray *visibleEffectViews = [self.qmui_effectViews qmui_filterWithBlock:^BOOL(UIVisualEffectView * _Nonnull item) { + return !item.hidden && item.alpha > 0.01 && item.superview; + }]; + return visibleEffectViews.lastObject; +} + +- (NSArray *)qmui_effectViews { + UIView *backgroundView = self.qmui_backgroundView; + NSMutableArray *result = NSMutableArray.new; + if (@available(iOS 13.0, *)) { + UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"]; + if (backgroundEffectView1) { + [result addObject:backgroundEffectView1]; + } + if (backgroundEffectView2) { + [result addObject:backgroundEffectView2]; + } + } else { + UIVisualEffectView *backgroundEffectView = [backgroundView qmui_valueForKey:@"_backgroundEffectView"]; + if (backgroundEffectView) { + [result addObject:backgroundEffectView]; + } + } + return result.count > 0 ? result : nil; +} + +static char kAssociatedObjectKey_effect; +- (void)setQmui_effect:(UIBlurEffect *)qmui_effect { + if (qmui_effect) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + + BOOL valueChanged = self.qmui_effect != qmui_effect; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effect, qmui_effect, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffect = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIBlurEffect *)qmui_effect { + return (UIBlurEffect *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effect); +} + +static char kAssociatedObjectKey_effectForegroundColor; +- (void)setQmui_effectForegroundColor:(UIColor *)qmui_effectForegroundColor { + if (qmui_effectForegroundColor) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + BOOL valueChanged = ![self.qmui_effectForegroundColor isEqual:qmui_effectForegroundColor]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor, qmui_effectForegroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffectForegroundColor = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIColor *)qmui_effectForegroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor); +} + +@end diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h new file mode 100644 index 00000000..c7e00883 --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h @@ -0,0 +1,17 @@ +// +// UITabBar+QMUIBarProtocol.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import +#import "QMUIBarProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UITabBar (QMUIBarProtocol) +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m new file mode 100644 index 00000000..f234f10b --- /dev/null +++ b/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m @@ -0,0 +1,124 @@ +// +// UITabBar+QMUIBarProtocol.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import "UITabBar+QMUIBarProtocol.h" +#import "QMUIBarProtocolPrivate.h" +#import "QMUICore.h" +#import "UIVisualEffectView+QMUI.h" +#import "NSArray+QMUI.h" + +@interface UITabBar () +@end + +@implementation UITabBar (QMUIBarProtocol) + +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffect, setQmuibar_hasSetEffect) +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffectForegroundColor, setQmuibar_hasSetEffectForegroundColor) + +BeginIgnoreClangWarning(-Wobjc-protocol-method-implementation) +- (void)qmuibar_updateEffect { + [self.qmui_effectViews enumerateObjectsUsingBlock:^(UIVisualEffectView * _Nonnull effectView, NSUInteger idx, BOOL * _Nonnull stop) { + if (self.qmuibar_hasSetEffect) { + // 这里对 iOS 13 不使用 UITabBarAppearance.backgroundEffect 来修改,是因为反正不管 iOS 10 还是 13,最终都是 setBackgroundEffects: 在起作用,而且不用 UITabBarAppearance 还可以规避与 UIAppearance 机制的冲突 + NSArray *effects = self.qmuibar_backgroundEffects; + [effectView qmui_performSelector:NSSelectorFromString(@"setBackgroundEffects:") withArguments:&effects, nil]; + } + if (self.qmuibar_hasSetEffectForegroundColor) { + effectView.qmui_foregroundColor = self.qmui_effectForegroundColor; + } + }]; +} +EndIgnoreClangWarning + +// UITabBar、UIVisualEffectView 都有一个私有的方法 backgroundEffects,当 UIVisualEffectView 应用于 UITabBar 场景时,磨砂的效果实际上被放在 backgroundEffects 内,而不是公开接口的 effect 属性里,这里为了方便,将 UITabBar (QMUI).effect 转成可用于 backgroundEffects 的数组 +- (NSArray *)qmuibar_backgroundEffects { + if (self.qmuibar_hasSetEffect) { + return self.qmui_effect ? @[self.qmui_effect] : nil; + } + return nil; +} + +#pragma mark - + +- (UIView *)qmui_backgroundView { + return [self qmui_valueForKey:@"_backgroundView"]; +} + +- (UIImageView *)qmui_shadowImageView { + // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 + if (@available(iOS 13, *)) { + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; + } + // iOS 10 及以后,在 bar 初始化之后就能获取到 backgroundView 和 shadowView 了 + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"]; +} + +- (UIVisualEffectView *)qmui_effectView { + NSArray *visibleEffectViews = [self.qmui_effectViews qmui_filterWithBlock:^BOOL(UIVisualEffectView * _Nonnull item) { + return !item.hidden && item.alpha > 0.01 && item.superview; + }]; + return visibleEffectViews.lastObject; +} + +- (NSArray *)qmui_effectViews { + UIView *backgroundView = self.qmui_backgroundView; + NSMutableArray *result = NSMutableArray.new; + if (@available(iOS 13.0, *)) { + UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"]; + if (backgroundEffectView1) { + [result addObject:backgroundEffectView1]; + } + if (backgroundEffectView2) { + [result addObject:backgroundEffectView2]; + } + } else { + UIVisualEffectView *backgroundEffectView = [backgroundView qmui_valueForKey:@"_backgroundEffectView"]; + if (backgroundEffectView) { + [result addObject:backgroundEffectView]; + } + } + return result.count > 0 ? result : nil; +} + +static char kAssociatedObjectKey_effect; +- (void)setQmui_effect:(UIBlurEffect *)qmui_effect { + if (qmui_effect) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + + BOOL valueChanged = self.qmui_effect != qmui_effect; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effect, qmui_effect, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffect = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIBlurEffect *)qmui_effect { + return (UIBlurEffect *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effect); +} + +static char kAssociatedObjectKey_effectForegroundColor; +- (void)setQmui_effectForegroundColor:(UIColor *)qmui_effectForegroundColor { + if (qmui_effectForegroundColor) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + BOOL valueChanged = ![self.qmui_effectForegroundColor isEqual:qmui_effectForegroundColor]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor, qmui_effectForegroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffectForegroundColor = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIColor *)qmui_effectForegroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor); +} + +@end diff --git a/QMUIKit/UIKitExtensions/QMUIStringPrivate.m b/QMUIKit/UIKitExtensions/QMUIStringPrivate.m index 5698c3ab..7b088422 100644 --- a/QMUIKit/UIKitExtensions/QMUIStringPrivate.m +++ b/QMUIKit/UIKitExtensions/QMUIStringPrivate.m @@ -70,11 +70,28 @@ + (NSRange)downRoundRangeOfComposedCharacterSequences:(NSRange)range inString:(N if (range.length == 0) { return range; } - NSRange result = range; - NSRange beginRange = [string rangeOfComposedCharacterSequenceAtIndex:range.location]; - result.location = beginRange.location < result.location ? NSMaxRange(beginRange) : result.location; - NSRange endRange = [string rangeOfComposedCharacterSequenceAtIndex:NSMaxRange(range)]; - result.length = endRange.location < NSMaxRange(range) ? endRange.location - result.location : NSMaxRange(range) - result.location; + NSRange systemRange = [string rangeOfComposedCharacterSequencesForRange:range];// 系统总是往大取值 + if (NSEqualRanges(range, systemRange)) { + return range; + } + NSRange result = systemRange; + if (range.location > systemRange.location) { + // 意味着传进来的 range 起点刚好在某个 Character Sequence 中间,所以要把这个 Character Sequence 遗弃掉,从它后面的字符开始算 + NSRange beginRange = [string rangeOfComposedCharacterSequenceAtIndex:range.location]; + result.location = NSMaxRange(beginRange); + result.length -= beginRange.length; + } + if (NSMaxRange(range) < NSMaxRange(systemRange)) { + // 意味着传进来的 range 终点刚好在某个 Character Sequence 中间,所以要把这个 Character Sequence 遗弃掉,只取到它前面的字符 + NSRange endRange = [string rangeOfComposedCharacterSequenceAtIndex:NSMaxRange(range) - 1]; + + // 如果参数传进来的 range 刚好落在一个 emoji 的中间,就会导致前面减完 beginRange 这里又减掉一个 endRange,出现负数(注意这里 length 是 NSUInteger),所以做个保护,可以用 👨‍👩‍👧‍👦 测试,这个 emoji 长度是 11 + if (result.length >= endRange.length) { + result.length = result.length - endRange.length; + } else { + result.length = 0; + } + } return result; } @@ -82,11 +99,11 @@ + (id)substring:(id)aString avoidBreakingUpCharacterSequencesFromIndex:(NSUInteg NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; NSString *string = attributedString.string ?: (NSString *)aString; NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; - QMUIAssert(index < length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); - if (index >= length) return nil; + QMUIAssert(index <= length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); + if (index >= length) return @""; index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultMode:index inString:string] : index;// 实际计算都按照系统默认的 length 规则来 NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index]; - index = lessValue ? NSMaxRange(range) : range.location; + index = range.length == 1 ? index : (lessValue ? NSMaxRange(range) : range.location); if (attributedString) { NSAttributedString *resultString = [attributedString attributedSubstringFromRange:NSMakeRange(index, string.length - index)]; return resultString; @@ -99,12 +116,12 @@ + (id)substring:(id)aString avoidBreakingUpCharacterSequencesToIndex:(NSUInteger NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; NSString *string = attributedString.string ?: (NSString *)aString; NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; - QMUIAssert(index < length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); - if (index == 0 || index > length) return nil; + QMUIAssert(index <= length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); + if (index == 0 || index > length) return @""; if (index == length) return [aString copy];// 根据系统 -[NSString substringToIndex:] 的注释,在 index 等于 length 时会返回 self 的 copy。 index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultMode:index inString:string] : index;// 实际计算都按照系统默认的 length 规则来 NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index]; - index = lessValue ? range.location : NSMaxRange(range); + index = range.length == 1 ? index : (lessValue ? range.location : NSMaxRange(range)); if (attributedString) { NSAttributedString *resultString = [attributedString attributedSubstringFromRange:NSMakeRange(0, index)]; return resultString; @@ -118,7 +135,7 @@ + (id)substring:(id)aString avoidBreakingUpCharacterSequencesWithRange:(NSRange) NSString *string = attributedString.string ?: (NSString *)aString; NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; QMUIAssert(NSMaxRange(range) <= length, @"QMUIStringPrivate", @"%s, range %@ out of bounds. string = %@", __func__, NSStringFromRange(range), attributedString ?: string); - if (NSMaxRange(range) > length) return nil; + if (NSMaxRange(range) > length) return @""; range = countingNonASCIICharacterAsTwo ? [self transformRangeToDefaultMode:range lessValue:lessValue inString:string] : range;// 实际计算都按照系统默认的 length 规则来 NSRange characterSequencesRange = lessValue ? [self downRoundRangeOfComposedCharacterSequences:range inString:string] : [string rangeOfComposedCharacterSequencesForRange:range]; if (attributedString) { @@ -229,7 +246,7 @@ + (void)qmuisafety_NSString { // 继承关系是 __NSCFConstantString → __NSCFString → NSMutableString → NSString,其中 __NSCFString 重写了 substringWithRange:(其他 substring 方法没任何人重写),所以这里要 hook __NSCFString 而不是 NSString OverrideImplementation(NSClassFromString(@"__NSCFString"), @selector(substringWithRange:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^NSString *(NSString *selfObject, NSRange range) { - // range 越界 + // range 越界,注意这里识别不了负值,例如一个 (10, -8) 的 range,它的 NSMaxRange 返回2,会认为长度小于 length 所以合法,但实际上是非法的,所以交给下面的流程专门识别。 { BOOL isValidddatedRange = NSMaxRange(range) <= selfObject.length; if (!isValidddatedRange) { @@ -239,6 +256,17 @@ + (void)qmuisafety_NSString { } } + // rang 负值 + { + NSInteger location = range.location; + NSInteger length = range.length; + if (location < 0 || length < 0) { + NSString *logString = [NSString stringWithFormat:@"%@ 传入了一个可能由负数转换过来的 range: %@,猜测转换前数值为 (%@, %@),原字符串为: %@(%@)", NSStringFromSelector(originCMD), NSStringFromRange(range), @(location), @(length), selfObject, @(selfObject.length)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); +// return @"";// 由于理论上不可能准确识别这种情况,所以这里不干预 return 值,只是做个 assert 提醒 + } + } + // 保护从 emoji 等 ComposedCharacterSequence 中间裁剪的场景 { if (NSMaxRange(range) < selfObject.length) { diff --git a/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h b/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h index f7e4c5b3..3c2257b6 100644 --- a/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h +++ b/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h @@ -21,6 +21,7 @@ * 创建一个指定大小的UIActivityIndicatorView * * 系统的UIActivityIndicatorView尺寸是由UIActivityIndicatorViewStyle决定的,固定不变。因此创建后通过CGAffineTransformMakeScale将其缩放到指定大小。self.frame获取的值也是缩放后的值,不影响布局。 + * init 后也可以通过 UIView(QMUI).qmui_size 修改大小。 * * @param style UIActivityIndicatorViewStyle * @param size UIActivityIndicatorView的大小 diff --git a/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m b/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m index d9830ce1..cd2e5ea4 100644 --- a/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m @@ -14,16 +14,22 @@ // #import "UIActivityIndicatorView+QMUI.h" +#import "UIView+QMUI.h" @implementation UIActivityIndicatorView (QMUI) - (instancetype)initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style size:(CGSize)size { if (self = [self initWithActivityIndicatorStyle:style]) { - CGSize initialSize = self.bounds.size; - CGFloat scale = size.width / initialSize.width; - self.transform = CGAffineTransformMakeScale(scale, scale); + self.qmui_size = size; } return self; } +- (void)setQmui_size:(CGSize)size { +// [super setQmui_size:qmui_size]; + CGSize initialSize = self.bounds.size; + CGFloat scale = size.width / initialSize.width; + self.transform = CGAffineTransformMakeScale(scale, scale); +} + @end diff --git a/QMUIKit/UIKitExtensions/UILabel+QMUI.m b/QMUIKit/UIKitExtensions/UILabel+QMUI.m index 3a95694f..e6d19b64 100644 --- a/QMUIKit/UIKitExtensions/UILabel+QMUI.m +++ b/QMUIKit/UIKitExtensions/UILabel+QMUI.m @@ -224,6 +224,13 @@ - (CGFloat)qmui_lineHeight { return result == 0 ? self.font.lineHeight : result; } else if (self.text.length) { return self.font.lineHeight; + } else if (self.qmui_textAttributes) { + // 当前 label 连文字都没有时,再尝试从 qmui_textAttributes 里获取 + if ([self.qmui_textAttributes.allKeys containsObject:NSParagraphStyleAttributeName]) { + return ((NSParagraphStyle *)self.qmui_textAttributes[NSParagraphStyleAttributeName]).minimumLineHeight; + } else if ([self.qmui_textAttributes.allKeys containsObject:NSFontAttributeName]) { + return ((UIFont *)self.qmui_textAttributes[NSFontAttributeName]).lineHeight; + } } return 0; diff --git a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h index 175cfff0..7226ec6b 100644 --- a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h +++ b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h @@ -24,31 +24,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, strong, readonly, nullable) UIView *qmui_contentView; -/** - UINavigationBar 的背景 view,可能显示磨砂、背景图,顶部有一部分溢出到 UINavigationBar 外。 - - 在 iOS 10 及以后是私有的 _UIBarBackground 类。 - - 在 iOS 9 及以前是私有的 _UINavigationBarBackground 类。 - */ -@property(nonatomic, strong, readonly, nullable) UIView *qmui_backgroundView; - -/** - qmui_backgroundView 内显示实际背景的 view,可能是磨砂或者背景图片。 - - 在 iOS 10 及以后,该 view 为 qmui_backgroundView 的 subview,当显示磨砂时是一个 UIVisualEffectView,当显示背景图时是一个 UIImageView。 - - 在 iOS 9 及以前,如果显示磨砂,该 view 为 qmui_backgroundView 的 subview,是一个 _UIBackdropView,如果显示背景图,则返回 qmui_backgroundView 自身,因为 _UINavigationBarBackground 本身就是一个 UIImageView。 - - @warning 如果要以 view 的方式去修改 UINavigationBar 的背景,由于不同的 iOS 版本,qmui_shadowImageView 和 qmui_backgroundContentView 的层级关系不同,所以为了效果的统一,建议这种情况下操作 qmui_backgroundView 会好过于操作 qmui_backgroundContentView。 - */ -@property(nonatomic, strong, readonly, nullable) __kindof UIView *qmui_backgroundContentView; - -/** - qmui_backgroundView 内的 subview,用于显示底部分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 - */ -@property(nonatomic, strong, readonly, nullable) UIImageView *qmui_shadowImageView; - @end NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m index 28e0fe72..ae03f590 100644 --- a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m @@ -212,31 +212,49 @@ + (void)load { // return result; // }; // }); + + OverrideImplementation([UINavigationBar class], @selector(setTitleTextAttributes:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, NSDictionary *titleTextAttributes) { + + // call super + void (*originSelectorIMP)(id, SEL, NSDictionary *); + originSelectorIMP = (void (*)(id, SEL, NSDictionary *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, titleTextAttributes); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.titleTextAttributes = titleTextAttributes; + }); + }; + }); } if (@available(iOS 15.0, *)) { - if (QMUICMIActivated && (NavBarRemoveBackgroundEffectAutomatically || TabBarRemoveBackgroundEffectAutomatically || ToolBarRemoveBackgroundEffectAutomatically)) { - // - [_UIBarBackground updateBackground] - OverrideImplementation(NSClassFromString(@"_UIBarBackground"), NSSelectorFromString(@"updateBackground"), ^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); - - if (!selfObject.superview) return; - if (!NavBarRemoveBackgroundEffectAutomatically && [selfObject.superview isKindOfClass:UINavigationBar.class]) return; - if (!TabBarRemoveBackgroundEffectAutomatically && [selfObject.superview isKindOfClass:UITabBar.class]) return; - if (!ToolBarRemoveBackgroundEffectAutomatically && [selfObject.superview isKindOfClass:UIToolbar.class]) return; - - UIImageView *backgroundImageView1 = [selfObject valueForKey:@"_colorAndImageView1"]; - UIImageView *backgroundImageView2 = [selfObject valueForKey:@"_colorAndImageView2"]; - UIVisualEffectView *backgroundEffectView1 = [selfObject valueForKey:@"_effectView1"]; - UIVisualEffectView *backgroundEffectView2 = [selfObject valueForKey:@"_effectView2"]; - - // iOS 14 系统默认特性是存在 backgroundImage 则不存在其他任何背景,但如果存在 barTintColor 则磨砂 view 也可以共存。 - // iOS 15 系统默认特性是 backgroundImage、backgroundColor、backgroundEffect 三者都可以共存,其中前两者共用 _colorAndImageView,而我们这个开关为了符合 iOS 14 的特性,仅针对 _colorAndImageView 是因为 backgroundImage 存在而出现的情况做处理。 + if (!QMUICMIActivated) return; + if (!(NavBarRemoveBackgroundEffectAutomatically || TabBarRemoveBackgroundEffectAutomatically || ToolBarRemoveBackgroundEffectAutomatically) + && !(NavBarUsesStandardAppearanceOnly || TabBarUsesStandardAppearanceOnly || ToolBarUsesStandardAppearanceOnly)) return; + + // - [_UIBarBackground updateBackground] + OverrideImplementation(NSClassFromString(@"_UIBarBackground"), NSSelectorFromString(@"updateBackground"), ^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); + + if (!selfObject.superview) return; + if (!NavBarRemoveBackgroundEffectAutomatically && !NavBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UINavigationBar.class]) return; + if (!TabBarRemoveBackgroundEffectAutomatically && !TabBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UITabBar.class]) return; + if (!ToolBarRemoveBackgroundEffectAutomatically && !ToolBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UIToolbar.class]) return; + + UIImageView *backgroundImageView1 = [selfObject valueForKey:@"_colorAndImageView1"]; + UIImageView *backgroundImageView2 = [selfObject valueForKey:@"_colorAndImageView2"]; + UIVisualEffectView *backgroundEffectView1 = [selfObject valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [selfObject valueForKey:@"_effectView2"]; + + // iOS 14 系统默认特性是存在 backgroundImage 则不存在其他任何背景,但如果存在 barTintColor 则磨砂 view 也可以共存。 + // iOS 15 系统默认特性是 backgroundImage、backgroundColor、backgroundEffect 三者都可以共存,其中前两者共用 _colorAndImageView,而我们这个开关为了符合 iOS 14 的特性,仅针对 _colorAndImageView 是因为 backgroundImage 存在而出现的情况做处理。 + if (NavBarRemoveBackgroundEffectAutomatically || TabBarRemoveBackgroundEffectAutomatically || ToolBarRemoveBackgroundEffectAutomatically) { BOOL hasBackgroundImage1 = backgroundImageView1 && backgroundImageView1.superview && !backgroundImageView1.hidden && backgroundImageView1.image; BOOL hasBackgroundImage2 = backgroundImageView2 && backgroundImageView2.superview && !backgroundImageView2.hidden && backgroundImageView2.image; BOOL shouldHideEffectView = hasBackgroundImage1 || hasBackgroundImage2; @@ -246,9 +264,15 @@ + (void)load { } else { // 把 backgroundImage 置为 nil,理应要恢复 effectView 的显示,但由于 iOS 15 里 effectView 有2个,什么时候显示哪个取决于 contentScrollView 的滚动位置,而这个位置在当前上下文里我们是无法得知的,所以先不处理了,交给系统在下一次 updateBackground 时刷新吧... } - }; - }); - } + } + + // 虽然 4.4.0 增加的这些开关会保证 scrollEdgeAppearance 也被设置,但系统始终都会同时显示两份 view(一份 standard 的一份 scrollEdge 的),当你的样式是不透明时没问题,但如果存在半透明,同时显示两份 view 就会导致两个半透明的效果重叠在一起,最终肉眼看到的样式和预期是不符合的,所以 4.4.4 开始,我们会强制让其中一份 view 隐藏掉。 + if (NavBarUsesStandardAppearanceOnly || TabBarUsesStandardAppearanceOnly || ToolBarUsesStandardAppearanceOnly) { + backgroundImageView2.hidden = YES; + backgroundEffectView2.hidden = YES; + } + }; + }); } #endif }); @@ -258,28 +282,4 @@ - (UIView *)qmui_contentView { return [self valueForKeyPath:@"visualProvider.contentView"]; } -- (UIView *)qmui_backgroundView { - return [self qmui_valueForKey:@"_backgroundView"]; -} - -- (__kindof UIView *)qmui_backgroundContentView { - if (@available(iOS 13, *)) { - return [self.qmui_backgroundView qmui_valueForKey:@"_colorAndImageView1"]; - } else { - UIImageView *imageView = [self.qmui_backgroundView qmui_valueForKey:@"_backgroundImageView"]; - UIVisualEffectView *visualEffectView = [self.qmui_backgroundView qmui_valueForKey:@"_backgroundEffectView"]; - UIView *customView = [self.qmui_backgroundView qmui_valueForKey:@"_customBackgroundView"]; - UIView *result = customView && customView.superview ? customView : (imageView && imageView.superview ? imageView : visualEffectView); - return result; - } -} - -- (UIImageView *)qmui_shadowImageView { - // UINavigationBar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 - if (@available(iOS 13, *)) { - return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; - } - return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"]; -} - @end diff --git a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m index a0b73587..be7ab4ca 100644 --- a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m @@ -252,10 +252,22 @@ + (void)load { [selfObject setQmui_navigationAction:QMUINavigationActionDidPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; - [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + void (^transitionCompletion)(void) = ^void(void) { [selfObject setQmui_navigationAction:QMUINavigationActionPopCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; - }]; + }; + if (!result) { + // 如果系统的 pop 没有成功,实际上提交给 animateAlongsideTransition:completion: 的 completion 并不会被执行,所以这里改为手动调用 + if (transitionCompletion) { + transitionCompletion(); + } + } else { + [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + if (transitionCompletion) { + transitionCompletion(); + } + }]; + } return result; }; @@ -530,7 +542,8 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { BOOL canPopViewController = [self.parentViewController canPopViewController:self.parentViewController.topViewController byPopGesture:YES]; if (canPopViewController) { if ([self.parentViewController.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { - return [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer]; + BOOL result = [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer]; + return result; } else { return NO; } @@ -558,7 +571,8 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { if ([self.parentViewController.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { - return [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer]; + BOOL result = [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer]; + return result; } } return NO; diff --git a/QMUIKit/UIKitExtensions/UISlider+QMUI.m b/QMUIKit/UIKitExtensions/UISlider+QMUI.m index f9de26e0..714164bc 100644 --- a/QMUIKit/UIKitExtensions/UISlider+QMUI.m +++ b/QMUIKit/UIKitExtensions/UISlider+QMUI.m @@ -31,6 +31,7 @@ @implementation UISlider (QMUI) QMUISynthesizeIdStrongProperty(qmuisl_stepControls, setQmuisl_stepControls) QMUISynthesizeIdCopyProperty(qmuisl_layoutCachedKey, setQmuisl_layoutCachedKey) QMUISynthesizeNSUIntegerProperty(qmuisl_precedingStep, setQmuisl_precedingStep) +QMUISynthesizeIdCopyProperty(qmui_stepDidChangeBlock, setQmui_stepDidChangeBlock) - (UIView *)qmui_thumbView { // thumbView 并非在一开始就存在,而是在某个时机才生成的。如果使用了自己的 thumbImage,则系统用 _thumbView 来显示。如果没用自己的 thumbImage,则系统用 _innerThumbView 来存放。注意如果是 _innerThumbView,它外部还有一个 _thumbViewNeue 用来控制布局。 @@ -112,7 +113,7 @@ - (void)setQmui_thumbShadowColor:(UIColor *)qmui_thumbShadowColor { [QMUIHelper executeBlock:^{ if (@available(iOS 14.0, *)) { // -[_UISlideriOSVisualElement didAddSubview:] - OverrideImplementation(NSClassFromString(@"_UISlideriOSVisualElement"), @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UISlider", @"iOS", @"VisualElement", nil]), @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, UIView *subview) { // call super @@ -199,6 +200,7 @@ - (void)setQmui_numberOfSteps:(NSUInteger)numberOfSteps { [obj removeFromSuperview]; }]; self.qmuisl_stepControls = nil; + [self removeTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; return; } @@ -231,6 +233,9 @@ - (void)setQmui_numberOfSteps:(NSUInteger)numberOfSteps { }]; } [self qmuisl_setNeedsLayout]; + + [self removeTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; + [self addTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; } - (NSUInteger)qmui_numberOfSteps { @@ -264,25 +269,16 @@ - (void)setQmui_stepControlConfiguration:(void (^)(__kindof UISlider * _Nonnull, return (void (^)(UISlider * _Nonnull, QMUISliderStepControl * _Nonnull, NSUInteger))objc_getAssociatedObject(self, &kAssociatedObjectKey_stepControlConfiguration); } -static char kAssociatedObjectKey_stepDidChangeBlock; -- (void)setQmui_stepDidChangeBlock:(void (^)(__kindof UISlider * _Nonnull, NSUInteger))stepDidChangeBlock { - objc_setAssociatedObject(self, &kAssociatedObjectKey_stepDidChangeBlock, stepDidChangeBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - [self removeTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; - if (stepDidChangeBlock) { - [self addTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; - } -} - -- (void (^)(__kindof UISlider * _Nonnull, NSUInteger))qmui_stepDidChangeBlock { - return (void (^)(__kindof UISlider * _Nonnull, NSUInteger))objc_getAssociatedObject(self, &kAssociatedObjectKey_stepDidChangeBlock); -} - - (void)qmuisl_handleValueChanged:(UISlider *)slider { - if (!slider.qmui_stepDidChangeBlock || slider.qmui_numberOfSteps < 2) return; + if (slider.qmui_numberOfSteps < 2) return; NSUInteger step = [slider qmuisl_stepWithValue:slider.value]; if (step != slider.qmuisl_precedingStep) { - slider.qmui_stepDidChangeBlock(slider, slider.qmuisl_precedingStep); + if (slider.qmui_stepDidChangeBlock) { + slider.qmui_stepDidChangeBlock(slider, slider.qmuisl_precedingStep); + } + // 即便不存在 qmui_stepDidChangeBlock 也要记录 precedingStep + // https://github.com/Tencent/QMUI_iOS/issues/1413 slider.qmuisl_precedingStep = step; } } diff --git a/QMUIKit/UIKitExtensions/UITabBar+QMUI.h b/QMUIKit/UIKitExtensions/UITabBar+QMUI.h index 94fb0d80..6615e4c7 100644 --- a/QMUIKit/UIKitExtensions/UITabBar+QMUI.h +++ b/QMUIKit/UIKitExtensions/UITabBar+QMUI.h @@ -14,48 +14,10 @@ // #import +#import "QMUIBarProtocol.h" NS_ASSUME_NONNULL_BEGIN -@interface UITabBar (QMUI) - -/** - UITabBar 的背景 view,可能显示磨砂、背景图,顶部有一部分溢出到 UITabBar 外。 - - 在 iOS 10 及以后是私有的 _UIBarBackground 类。 - - 在 iOS 9 及以前是私有的 _UITabBarBackgroundView 类。 - */ -@property(nullable, nonatomic, strong, readonly) UIView *qmui_backgroundView; - -/** - qmui_backgroundView 内的 subview,用于显示顶部分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 - */ -@property(nullable, nonatomic, strong, readonly) UIImageView *qmui_shadowImageView; - -/** - 获取 tabBar 里面的磨砂背景,具体的 view 层级是 UITabBar → _UIBarBackground → UIVisualEffectView。仅在 tabBar 的样式确定之后系统才会创建。 - iOS 15 及以后,tabBar 里可能会同时存在多个磨砂背景(详见 @c qmui_effectViews ),这个属性会获取其中正在显示的那个磨砂,如果两个都在显示,则取 view 层级树里更上层的那个。 - */ -@property(nullable, nonatomic, strong, readonly) UIVisualEffectView *qmui_effectView; - -/** - iOS 15 及以后,由于 bar 的样式在滚动到顶部和底部会有不同,所以可能同时存在两个 effectView。 - */ -@property(nullable, nonatomic, strong, readonly) NSArray *qmui_effectViews; - -/** - 允许直接指定 tabBar 具体的磨砂样式(系统的仅在 iOS 13 及以后用 UITabBarAppearance.backgroundEffects 才可以实现)。默认为 nil,如果你没设置过这个属性,那么 nil 的行为就是维持系统的样式,但如果你主动设置过这个属性,那么后续的 nil 则表示把磨砂清空(也即可能出现背景透明的 bar)。 - @note 生效的前提是 backgroundImage、barTintColor 都为空,因为这两者的优先级都比磨砂高。 - */ -@property(nullable, nonatomic, strong) UIBlurEffect *qmui_effect; - -/** - 当 tabBar 展示磨砂的样式时,可以通过这个属性精准指定磨砂的前景色(可参考 CALayer(QMUI).qmui_foregroundColor),因为系统的某些 UIBlurEffectStyle 会自带前景色,且不可去掉,那种情况下你就无法得到准确的自定义前景色了(即便你试图通过设置半透明的 barTintColor 来达到前景色的效果,那也依然会叠加一层系统自带的半透明前景色)。 - */ -@property(nullable, nonatomic, strong) UIColor *qmui_effectForegroundColor; -@end - #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) @interface UITabBarAppearance (QMUI) diff --git a/QMUIKit/UIKitExtensions/UITabBar+QMUI.m b/QMUIKit/UIKitExtensions/UITabBar+QMUI.m index c4a9be23..82fe15eb 100644 --- a/QMUIKit/UIKitExtensions/UITabBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITabBar+QMUI.m @@ -14,15 +14,14 @@ // #import "UITabBar+QMUI.h" +#import "UITabBar+QMUIBarProtocol.h" #import "QMUICore.h" #import "UITabBarItem+QMUI.h" #import "UIBarItem+QMUI.h" #import "UIImage+QMUI.h" #import "UIView+QMUI.h" #import "UINavigationController+QMUI.h" -#import "UIVisualEffectView+QMUI.h" #import "UIApplication+QMUI.h" -#import "NSArray+QMUI.h" NSInteger const kLastTouchedTabBarItemIndexNone = -1; NSString *const kShouldCheckTabBarHiddenKey = @"kShouldCheckTabBarHiddenKey"; @@ -32,8 +31,6 @@ @interface UITabBar () @property(nonatomic, assign) BOOL canItemRespondDoubleTouch; @property(nonatomic, assign) NSInteger lastTouchedTabBarItemViewIndex; @property(nonatomic, assign) NSInteger tabBarItemViewTouchCount; -@property(nonatomic, assign) BOOL qmuitb_hasSetEffect; -@property(nonatomic, assign) BOOL qmuitb_hasSetEffectForegroundColor; @end @implementation UITabBar (QMUI) @@ -41,8 +38,6 @@ @implementation UITabBar (QMUI) QMUISynthesizeBOOLProperty(canItemRespondDoubleTouch, setCanItemRespondDoubleTouch) QMUISynthesizeNSIntegerProperty(lastTouchedTabBarItemViewIndex, setLastTouchedTabBarItemViewIndex) QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouchCount) -QMUISynthesizeBOOLProperty(qmuitb_hasSetEffect, setQmuitb_hasSetEffect) -QMUISynthesizeBOOLProperty(qmuitb_hasSetEffectForegroundColor, setQmuitb_hasSetEffectForegroundColor) + (void)load { static dispatch_once_t onceToken; @@ -402,7 +397,7 @@ + (void)load { syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) { itemAppearance.normal.iconColor = tintColor; - NSMutableDictionary *textAttributes = itemAppearance.selected.titleTextAttributes.mutableCopy; + NSMutableDictionary *textAttributes = itemAppearance.normal.titleTextAttributes.mutableCopy; textAttributes[NSForegroundColorAttributeName] = tintColor; itemAppearance.normal.titleTextAttributes = textAttributes.copy; }); @@ -429,18 +424,6 @@ + (void)load { }); } -- (UIView *)qmui_backgroundView { - return [self qmui_valueForKey:@"_backgroundView"]; -} - -- (UIImageView *)qmui_shadowImageView { - if (@available(iOS 13, *)) { - return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; - } - // iOS 10 及以后,在 UITabBar 初始化之后就能获取到 backgroundView 和 shadowView 了 - return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"]; -} - - (void)handleTabBarItemViewEvent:(UIControl *)itemView { if (!self.canItemRespondDoubleTouch) { @@ -484,130 +467,6 @@ - (void)revertTabBarItemTouch { self.tabBarItemViewTouchCount = 0; } -- (UIVisualEffectView *)qmui_effectView { - NSArray *visibleEffectViews = [self.qmui_effectViews qmui_filterWithBlock:^BOOL(UIVisualEffectView * _Nonnull item) { - return !item.hidden && item.alpha > 0.01 && item.superview; - }]; - return visibleEffectViews.lastObject; -} - -- (NSArray *)qmui_effectViews { - UIView *backgroundView = self.qmui_backgroundView; - NSMutableArray *result = NSMutableArray.new; - if (@available(iOS 13.0, *)) { - UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"]; - UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"]; - if (backgroundEffectView1) { - [result addObject:backgroundEffectView1]; - } - if (backgroundEffectView2) { - [result addObject:backgroundEffectView2]; - } - } else { - UIVisualEffectView *backgroundEffectView = [backgroundView qmui_valueForKey:@"_backgroundEffectView"]; - if (backgroundEffectView) { - [result addObject:backgroundEffectView]; - } - } - return result.count > 0 ? result : nil; -} - -- (void)qmuitb_swizzleBackgroundView { - [QMUIHelper executeBlock:^{ - Class backgroundClass = NSClassFromString(@"_UIBarBackground"); - OverrideImplementation(backgroundClass, @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIView *subview) { - - // call super - void (*originSelectorIMP)(id, SEL, UIView *); - originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, subview); - - // 注意可能存在多个 UIVisualEffectView,例如用于 shadowImage 的 _UIBarBackgroundShadowView,需要过滤掉 - if ([selfObject.superview isKindOfClass:UITabBar.class] && [subview isMemberOfClass:UIVisualEffectView.class]) { - UITabBar *tabBar = (UITabBar *)selfObject.superview; - if (tabBar.qmuitb_hasSetEffect || tabBar.qmuitb_hasSetEffectForegroundColor) { - [tabBar qmuitb_updateEffect]; - } - } - }; - }); - // 系统会在任意可能的时机去刷新 backgroundEffects,为了避免被系统的值覆盖,这里需要重写它 - OverrideImplementation(UIVisualEffectView.class, NSSelectorFromString(@"setBackgroundEffects:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIVisualEffectView *selfObject, NSArray *firstArgv) { - - if ([selfObject.superview isKindOfClass:backgroundClass] && [selfObject.superview.superview isKindOfClass:UITabBar.class]) { - UITabBar *tabBar = (UITabBar *)selfObject.superview.superview; - if (tabBar.qmui_effectView == selfObject) { - if (tabBar.qmuitb_hasSetEffect) { - firstArgv = tabBar.qmuitb_backgroundEffects; - } - } - } - - // call super - void (*originSelectorIMP)(id, SEL, NSArray *); - originSelectorIMP = (void (*)(id, SEL, NSArray *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); - }; - }); - } oncePerIdentifier:@"UITabBar (QMUI) effect"]; -} - -- (void)qmuitb_updateEffect { - if (self.qmuitb_hasSetEffect) { - // 这里对 iOS 13 不使用 UITabBarAppearance.backgroundEffect 来修改,是因为反正不管 iOS 10 还是 13,最终都是 setBackgroundEffects: 在起作用,而且不用 UITabBarAppearance 还可以规避与 UIAppearance 机制的冲突 - NSArray *effects = self.qmuitb_backgroundEffects; - [self.qmui_effectView qmui_performSelector:NSSelectorFromString(@"setBackgroundEffects:") withArguments:&effects, nil]; - } - if (self.qmuitb_hasSetEffectForegroundColor) { - self.qmui_effectView.qmui_foregroundColor = self.qmui_effectForegroundColor; - } -} - -// UITabBar、UIVisualEffectView 都有一个私有的方法 backgroundEffects,当 UIVisualEffectView 应用于 UITabBar 场景时,磨砂的效果实际上被放在 backgroundEffects 内,而不是公开接口的 effect 属性里,这里为了方便,将 UITabBar (QMUI).effect 转成可用于 backgroundEffects 的数组 -- (NSArray *)qmuitb_backgroundEffects { - if (self.qmuitb_hasSetEffect) { - return self.qmui_effect ? @[self.qmui_effect] : nil; - } - return nil; -} - -static char kAssociatedObjectKey_effect; -- (void)setQmui_effect:(UIBlurEffect *)qmui_effect { - if (qmui_effect) { - [self qmuitb_swizzleBackgroundView]; - } - - BOOL valueChanged = self.qmui_effect != qmui_effect; - objc_setAssociatedObject(self, &kAssociatedObjectKey_effect, qmui_effect, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (valueChanged) { - self.qmuitb_hasSetEffect = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 - [self qmuitb_updateEffect]; - } -} - -- (UIBlurEffect *)qmui_effect { - return (UIBlurEffect *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effect); -} - -static char kAssociatedObjectKey_effectForegroundColor; -- (void)setQmui_effectForegroundColor:(UIColor *)qmui_effectForegroundColor { - if (qmui_effectForegroundColor) { - [self qmuitb_swizzleBackgroundView]; - } - BOOL valueChanged = ![self.qmui_effectForegroundColor isEqual:qmui_effectForegroundColor]; - objc_setAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor, qmui_effectForegroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (valueChanged) { - self.qmuitb_hasSetEffectForegroundColor = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 - [self qmuitb_updateEffect]; - } -} - -- (UIColor *)qmui_effectForegroundColor { - return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor); -} - @end @implementation UITabBarAppearance (QMUI) diff --git a/QMUIKit/UIKitExtensions/UITableView+QMUI.h b/QMUIKit/UIKitExtensions/UITableView+QMUI.h index 0c4c2c47..0a585ab2 100644 --- a/QMUIKit/UIKitExtensions/UITableView+QMUI.h +++ b/QMUIKit/UIKitExtensions/UITableView+QMUI.h @@ -67,10 +67,10 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { */ - (NSInteger)qmui_indexForSectionHeaderAtView:(nullable UIView *)view; -/// 获取可视范围内的所有 sectionHeader 的 index +/// 获取可视范围内的所有 sectionHeader 的 index,注意 contentInset 所在的区域被视为“不可视”。 @property(nonatomic, readonly, nullable) NSArray *qmui_indexForVisibleSectionHeaders; -/// 获取正处于 pinned(悬停在顶部)状态的 sectionHeader 的序号 +/// 获取正处于 pinned(悬停在顶部)状态的 sectionHeader 的序号,注意如果某个 section 的 numberOfRows 为 0,则这个 section 天然无法被 pinned。 @property(nonatomic, readonly) NSInteger qmui_indexOfPinnedSectionHeader; /** diff --git a/QMUIKit/UIKitExtensions/UITableView+QMUI.m b/QMUIKit/UIKitExtensions/UITableView+QMUI.m index c27b874b..ac21d6e8 100644 --- a/QMUIKit/UIKitExtensions/UITableView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITableView+QMUI.m @@ -321,32 +321,13 @@ - (NSInteger)qmui_indexForSectionHeaderAtView:(UIView *)view { } - (NSArray *)qmui_indexForVisibleSectionHeaders { - NSArray *visibleCellIndexPaths = [self indexPathsForVisibleRows]; - if (!visibleCellIndexPaths.count) return nil; - // iOS 14 及以前的版本,只要某个 section 的 header 露出来了,该 section 里的第一个 cell 必定会出现在 visibleRows 内,但 iOS 15 必须是真的显示了 cell 才会出现在 visibleRows 里 // 这里针对 iOS 15 做个保护:如果最后一个可视 cell 刚好是该 section 的最后一个 cell,则检测范围再扩大到下一个 section,避免遗漏 -#ifdef IOS15_SDK_ALLOWED - if (@available(iOS 15.0, *)) { - NSIndexPath *lastVisibleIndexPath = visibleCellIndexPaths.lastObject; - if (lastVisibleIndexPath.row == [self numberOfRowsInSection:lastVisibleIndexPath.section] - 1 - && [self numberOfSections] > lastVisibleIndexPath.section + 1 - && [self numberOfRowsInSection:lastVisibleIndexPath.section + 1]) { - visibleCellIndexPaths = [visibleCellIndexPaths arrayByAddingObject:[NSIndexPath indexPathForRow:0 inSection:lastVisibleIndexPath.section + 1]]; - } - } -#endif - NSMutableArray *visibleSections = [[NSMutableArray alloc] init]; - NSMutableArray *result = [[NSMutableArray alloc] init]; - for (NSInteger i = 0; i < visibleCellIndexPaths.count; i++) { - if (visibleSections.count == 0 || visibleCellIndexPaths[i].section != visibleSections.lastObject.integerValue) { - [visibleSections addObject:@(visibleCellIndexPaths[i].section)]; - } - } - for (NSInteger i = 0; i < visibleSections.count; i++) { - NSInteger section = visibleSections[i].integerValue; + NSMutableArray *result = NSMutableArray.new; + NSInteger sections = self.numberOfSections; + for (NSInteger section = 0; section < sections; section++) { if ([self qmui_isHeaderVisibleForSection:section]) { - [result addObject:visibleSections[i]]; + [result addObject:@(section)]; } } if (result.count == 0) { @@ -377,21 +358,11 @@ - (BOOL)qmui_isHeaderPinnedForSection:(NSInteger)section { CGRect rectForHeader = [self rectForHeaderInSection:section]; BOOL isSectionScrollIntoContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top > CGRectGetMinY(rectForHeader);// 表示这个 section 已经往上滚动,超过 contentInset.top 那条线了 BOOL isSectionStayInContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top <= CGRectGetMaxY(rectForSection) - CGRectGetHeight(rectForHeader);// 表示这个 section 还没被完全滚走 -#ifdef IOS15_SDK_ALLOWED - if (@available(iOS 15.0, *)) { - // iOS 15 的系统交互发生变化,只有下一个 section 的 header 完全 pinned 后,上一个 section header 才会离开。iOS 14 及以前是下一个 section header 会边往上顶边把当前 section header 推走。所以这里要特殊处理一下。 - if (section + 1 < [self numberOfSections]) { - CGRect rectForNextHeader = [self rectForHeaderInSection:section + 1]; - isSectionStayInContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top < CGRectGetMinY(rectForNextHeader); - } - } -#endif BOOL isPinned = isSectionScrollIntoContentInsetTop && isSectionStayInContentInsetTop; return isPinned; } - (BOOL)qmui_isHeaderVisibleForSection:(NSInteger)section { - if (self.qmui_style != UITableViewStylePlain) return NO; if (section >= [self numberOfSections]) return NO; // 不存在 header 就不用判断 @@ -399,20 +370,17 @@ - (BOOL)qmui_isHeaderVisibleForSection:(NSInteger)section { if (CGRectGetHeight(rectForSectionHeader) <= 0) return NO; // 系统这个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect - CGRect rectForSection = [self rectForSection:section]; - BOOL isSectionScrollIntoBounds = CGRectGetMinY(rectForSectionHeader) < self.contentOffset.y + CGRectGetHeight(self.bounds); - BOOL isSectionStayInContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top < CGRectGetMaxY(rectForSection);// 表示这个 section 还没被完全滚走 -#ifdef IOS15_SDK_ALLOWED - if (@available(iOS 15.0, *)) { - // iOS 15 的系统交互发生变化,只有下一个 section 的 header 完全 pinned 后,上一个 section header 才会离开。iOS 14 及以前是下一个 section header 会边往上顶边把当前 section header 推走。所以这里要特殊处理一下。 - if (section + 1 < [self numberOfSections]) { - CGRect rectForNextHeader = [self rectForHeaderInSection:section + 1]; - isSectionStayInContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top < CGRectGetMinY(rectForNextHeader); - } + CGRect rectForSection = CGRectZero; + if (self.qmui_style == UITableViewStylePlain) { + rectForSection = [self rectForSection:section]; + } else { + rectForSection = [self rectForHeaderInSection:section]; } -#endif - BOOL isVisible = isSectionScrollIntoBounds && isSectionStayInContentInsetTop; - return isVisible; + CGRect visibleRect = CGRectMake(self.contentOffset.x + self.adjustedContentInset.left, self.contentOffset.y + self.adjustedContentInset.top, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.adjustedContentInset)); + if (CGRectIntersectsRect(visibleRect, rectForSection)) { + return YES; + } + return NO; } - (QMUITableViewCellPosition)qmui_positionForRowAtIndexPath:(NSIndexPath *)indexPath { diff --git a/QMUIKit/UIKitExtensions/UITextView+QMUI.m b/QMUIKit/UIKitExtensions/UITextView+QMUI.m index 2311a5ca..2b193c7d 100644 --- a/QMUIKit/UIKitExtensions/UITextView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITextView+QMUI.m @@ -15,6 +15,7 @@ #import "UITextView+QMUI.h" #import "QMUICore.h" +#import "UIScrollView+QMUI.h" @implementation UITextView (QMUI) @@ -88,21 +89,22 @@ - (void)_scrollRectToVisible:(CGRect)rect animated:(BOOL)animated { CGFloat contentOffsetY = self.contentOffset.y; - if (ABS(CGRectGetMinY(rect) - (contentOffsetY + self.textContainerInset.top)) <= 1) { - // 命中这个条件说明已经不用调整了,直接 return,避免继续走下面的判断,会重复调整,导致光标跳动 - // 一般情况下光标的 y 都比 textContainerInset.top 小1,所以这里用 <= 1 这个判断条件 - return; - } - - if (CGRectGetMinY(rect) < contentOffsetY + self.textContainerInset.top) { - // 光标在可视区域上方,往下滚动 - contentOffsetY = CGRectGetMinY(rect) - self.textContainerInset.top - self.contentInset.top; - } else if (CGRectGetMaxY(rect) > contentOffsetY + CGRectGetHeight(self.bounds) - self.textContainerInset.bottom - self.contentInset.bottom) { - // 光标在可视区域下方,往上滚动 - contentOffsetY = CGRectGetMaxY(rect) - CGRectGetHeight(self.bounds) + self.textContainerInset.bottom + self.contentInset.bottom; + BOOL canScroll = self.qmui_canScroll; + if (canScroll) { + if (CGRectGetMinY(rect) < contentOffsetY + self.textContainerInset.top) { + // 光标在可视区域上方,往下滚动 + contentOffsetY = CGRectGetMinY(rect) - self.textContainerInset.top - self.adjustedContentInset.top; + } else if (CGRectGetMaxY(rect) > contentOffsetY + CGRectGetHeight(self.bounds) - self.textContainerInset.bottom - self.adjustedContentInset.bottom) { + // 光标在可视区域下方,往上滚动 + contentOffsetY = CGRectGetMaxY(rect) - CGRectGetHeight(self.bounds) + self.textContainerInset.bottom + self.adjustedContentInset.bottom; + } else { + // 光标在可视区域,不用滚动 + } + CGFloat contentOffsetWhenScrollToTop = -self.adjustedContentInset.top; + CGFloat contentOffsetWhenScrollToBottom = self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds); + contentOffsetY = MAX(MIN(contentOffsetY, contentOffsetWhenScrollToBottom), contentOffsetWhenScrollToTop); } else { - // 光标在可视区域内,不用调整 - return; + contentOffsetY = -self.adjustedContentInset.top; } [self setContentOffset:CGPointMake(self.contentOffset.x, contentOffsetY) animated:animated]; } diff --git a/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m b/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m index 3d37c412..7a6fd0fd 100644 --- a/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m @@ -97,19 +97,19 @@ + (void)load { }; }); - OverrideImplementation([UIToolbar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIToolbar *selfObject, UIBarStyle barStyle) { - - // call super - void (*originSelectorIMP)(id, SEL, UIBarStyle); - originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, barStyle); - - syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { - appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; - }); - }; - }); +// OverrideImplementation([UIToolbar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { +// return ^(UIToolbar *selfObject, UIBarStyle barStyle) { +// +// // call super +// void (*originSelectorIMP)(id, SEL, UIBarStyle); +// originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider(); +// originSelectorIMP(selfObject, originCMD, barStyle); +// +// syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { +// appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; +// }); +// }; +// }); // iOS 15 没有对应的属性 // OverrideImplementation([UIToolbar class], @selector(barStyle), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { diff --git a/QMUIKit/UIKitExtensions/UIView+QMUI.m b/QMUIKit/UIKitExtensions/UIView+QMUI.m index 2049798f..2480b42b 100644 --- a/QMUIKit/UIKitExtensions/UIView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIView+QMUI.m @@ -33,8 +33,16 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], @selector(setTintColor:), UIColor *, ^(UIView *selfObject, UIColor *tintColor) { - selfObject.qmui_tintColorCustomized = !!tintColor; + OverrideImplementation([UIView class], @selector(setTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIColor *tintColor) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + + selfObject.qmui_tintColorCustomized = !!tintColor; + }; }); // 这个私有方法在 view 被调用 becomeFirstResponder 并且处于 window 上时,才会被调用,所以比 becomeFirstResponder 更适合用来检测 @@ -99,7 +107,7 @@ - (void)setQmui_outsideEdge:(UIEdgeInsets)qmui_outsideEdge { [QMUIHelper executeBlock:^{ if (@available(iOS 14.0, *)) { // -[_UISlideriOSVisualElement thumbHitEdgeInsets] - OverrideImplementation(NSClassFromString(@"_UISlideriOSVisualElement"), NSSelectorFromString(@"thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UISlider", @"iOS", @"VisualElement", nil]), NSSelectorFromString(@"thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIEdgeInsets(UIView *selfObject) { // call super UIEdgeInsets (*originSelectorIMP)(id, SEL); @@ -724,11 +732,19 @@ - (void)setQmui_sizeThatFitsBlock:(CGSize (^)(__kindof UIView * _Nonnull, CGSize // Extend 每个实例对象的类是为了保证比子类的 sizeThatFits 逻辑要更晚调用 Class viewClass = self.class; [QMUIHelper executeBlock:^{ - ExtendImplementationOfNonVoidMethodWithSingleArgument(viewClass, @selector(sizeThatFits:), CGSize, CGSize, ^CGSize(UIView *selfObject, CGSize firstArgv, CGSize originReturnValue) { - if (selfObject.qmui_sizeThatFitsBlock && [selfObject isMemberOfClass:viewClass]) { - originReturnValue = selfObject.qmui_sizeThatFitsBlock(selfObject, firstArgv, originReturnValue); - } - return originReturnValue; + OverrideImplementation(viewClass, @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGSize(UIView *selfObject, CGSize firstArgv) { + + // call super + CGSize (*originSelectorIMP)(id, SEL, CGSize); + originSelectorIMP = (CGSize (*)(id, SEL, CGSize))originalIMPProvider(); + CGSize result = originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmui_sizeThatFitsBlock && [selfObject isMemberOfClass:viewClass]) { + result = selfObject.qmui_sizeThatFitsBlock(selfObject, firstArgv, result); + } + return result; + }; }); } oncePerIdentifier:[NSString stringWithFormat:@"UIView %@-%@", NSStringFromClass(viewClass), NSStringFromSelector(@selector(sizeThatFits:))]]; } diff --git a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m index 0006d843..b180a884 100644 --- a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m @@ -190,18 +190,18 @@ - (NSString *)qmuivc_description { return [self qmuivc_description]; } - NSString *result = [NSString stringWithFormat:@"%@\nsuperclass:\t\t\t\t%@\ntitle:\t\t\t\t\t%@\nview:\t\t\t\t\t%@", [self qmuivc_description], NSStringFromClass(self.superclass), self.title, [self isViewLoaded] ? self.view : nil]; + NSString *result = [NSString stringWithFormat:@"%@; superclass: %@; title: %@; view: %@", [self qmuivc_description], NSStringFromClass(self.superclass), self.title, [self isViewLoaded] ? self.view : nil]; if ([self isKindOfClass:[UINavigationController class]]) { UINavigationController *navController = (UINavigationController *)self; - NSString *navDescription = [NSString stringWithFormat:@"\nviewControllers(%@):\t\t%@\ntopViewController:\t\t%@\nvisibleViewController:\t%@", @(navController.viewControllers.count), [self descriptionWithViewControllers:navController.viewControllers], [navController.topViewController qmuivc_description], [navController.visibleViewController qmuivc_description]]; + NSString *navDescription = [NSString stringWithFormat:@"; viewControllers(%@): %@; topViewController: %@; visibleViewController: %@", @(navController.viewControllers.count), [self descriptionWithViewControllers:navController.viewControllers], [navController.topViewController qmuivc_description], [navController.visibleViewController qmuivc_description]]; result = [result stringByAppendingString:navDescription]; } else if ([self isKindOfClass:[UITabBarController class]]) { UITabBarController *tabBarController = (UITabBarController *)self; - NSString *tabBarDescription = [NSString stringWithFormat:@"\nviewControllers(%@):\t\t%@\nselectedViewController(%@):\t%@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; + NSString *tabBarDescription = [NSString stringWithFormat:@"; viewControllers(%@): %@; selectedViewController(%@): %@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; result = [result stringByAppendingString:tabBarDescription]; } @@ -210,11 +210,11 @@ - (NSString *)qmuivc_description { - (NSString *)descriptionWithViewControllers:(NSArray *)viewControllers { NSMutableString *string = [[NSMutableString alloc] init]; - [string appendString:@"(\n"]; + [string appendString:@"( "]; for (NSInteger i = 0, l = viewControllers.count; i < l; i++) { - [string appendFormat:@"\t\t\t\t\t\t\t[%@]%@%@\n", @(i), [viewControllers[i] qmuivc_description], i < l - 1 ? @"," : @""]; + [string appendFormat:@"[%@]%@%@", @(i), [viewControllers[i] qmuivc_description], i < l - 1 ? @"," : @""]; } - [string appendString:@"\t\t\t\t\t\t)"]; + [string appendString:@" )"]; return [string copy]; } @@ -290,7 +290,7 @@ - (UIViewController *)qmui_visibleViewControllerIfExist { if ([self qmui_isViewLoadedAndVisible]) { return self; } else { - QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view : nil, [self isViewLoaded] ? self.view.window : nil); + QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view.window : nil); return nil; } } diff --git a/QMUIKitTests/UIKitExtensions/NSObjectTests.m b/QMUIKitTests/UIKitExtensions/NSObjectTests.m index dbaa8b88..72596bf6 100644 --- a/QMUIKitTests/UIKitExtensions/NSObjectTests.m +++ b/QMUIKitTests/UIKitExtensions/NSObjectTests.m @@ -26,10 +26,8 @@ - (void)testValueForKey { [navigationBar sizeToFit]; XCTAssertTrue(navigationBar.qmui_backgroundView); if (@available(iOS 13.0, *)) { - XCTAssertFalse(navigationBar.qmui_backgroundContentView); XCTAssertFalse(navigationBar.qmui_shadowImageView); } else { - XCTAssertTrue(navigationBar.qmui_backgroundContentView); XCTAssertTrue(navigationBar.qmui_shadowImageView); } diff --git a/qmui.xcodeproj/project.pbxproj b/qmui.xcodeproj/project.pbxproj index 3cf6c8b9..42bebe46 100644 --- a/qmui.xcodeproj/project.pbxproj +++ b/qmui.xcodeproj/project.pbxproj @@ -49,6 +49,13 @@ CD4EA4C02275FA0100A55066 /* NSMethodSignature+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA4BE2275FA0100A55066 /* NSMethodSignature+QMUI.m */; }; CD4EA576228C401E00A55066 /* QMUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */; }; CD4EA57E228C443B00A55066 /* UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA57D228C443B00A55066 /* UIColorTests.m */; }; + CD513E28283527AA004A549D /* QMUIBarProtocolPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */; }; + CD513E29283527AA004A549D /* QMUIBarProtocolPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */; }; + CD513E2A283527AA004A549D /* QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E27283527AA004A549D /* QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E2E283527CE004A549D /* UITabBar+QMUIBarProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */; }; + CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E32283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */; }; CD6631DB1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD6631DC1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */; }; CD669A0D25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -388,6 +395,13 @@ CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QMUIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CD4EA575228C401E00A55066 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CD4EA57D228C443B00A55066 /* UIColorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIColorTests.m; sourceTree = ""; }; + CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocolPrivate.h; sourceTree = ""; }; + CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIBarProtocolPrivate.m; sourceTree = ""; }; + CD513E27283527AA004A549D /* QMUIBarProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocol.h; sourceTree = ""; }; + CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITabBar+QMUIBarProtocol.h"; sourceTree = ""; }; + CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITabBar+QMUIBarProtocol.m"; sourceTree = ""; }; + CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+QMUIBarProtocol.h"; sourceTree = ""; }; + CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+QMUIBarProtocol.m"; sourceTree = ""; }; CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableViewHeaderFooterView.h; sourceTree = ""; }; CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITableViewHeaderFooterView.m; sourceTree = ""; }; CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewCell+QMUI.h"; sourceTree = ""; }; @@ -792,6 +806,20 @@ path = UIKitExtensions; sourceTree = ""; }; + CD513E24283527AA004A549D /* QMUIBarProtocol */ = { + isa = PBXGroup; + children = ( + CD513E27283527AA004A549D /* QMUIBarProtocol.h */, + CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */, + CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */, + CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */, + CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */, + CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */, + CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */, + ); + path = QMUIBarProtocol; + sourceTree = ""; + }; CD6BE1472058C61000BE093E /* QMUICellHeightKeyCache */ = { isa = PBXGroup; children = ( @@ -913,6 +941,7 @@ CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */, 1178D5672198258700AA30E5 /* NSURL+QMUI.h */, 1178D5682198258700AA30E5 /* NSURL+QMUI.m */, + CD513E24283527AA004A549D /* QMUIBarProtocol */, CD0A1BA8273512D5002A1A54 /* QMUIStringPrivate.h */, CD0A1BA9273512D5002A1A54 /* QMUIStringPrivate.m */, CDB8CA9A1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h */, @@ -1269,6 +1298,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */, + CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */, + CD513E2A283527AA004A549D /* QMUIBarProtocol.h in Headers */, CD70C43A276340B300D212F5 /* UISlider+QMUI.h in Headers */, CDE77517274FB9430066A767 /* UIBlurEffect+QMUI.h in Headers */, CDE77513274E93CE0066A767 /* UIToolbar+QMUI.h in Headers */, @@ -1332,6 +1364,7 @@ CD82C0AF206A2C3D0046EED2 /* QMUIMultipleDelegates.h in Headers */, CDD071FD2060F82700343AB6 /* QMUICellHeightCache.h in Headers */, D021DE37205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h in Headers */, + CD513E28283527AA004A549D /* QMUIBarProtocolPrivate.h in Headers */, D021DE3B205E80EB00FFA408 /* QMUICellSizeKeyCache.h in Headers */, CD6BE1562058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h in Headers */, CD6BE14E2058C64E00BE093E /* QMUICellHeightKeyCache.h in Headers */, @@ -1543,7 +1576,7 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = "/usr/bin/python ./umbrellaHeaderFileCreator.py"; + shellPath = "/usr/bin/python3 ./umbrellaHeaderFileCreator.py"; shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1589,6 +1622,7 @@ CD6BE14F2058C64E00BE093E /* QMUICellHeightKeyCache.m in Sources */, CDA4083F214F7E2500740888 /* NSCharacterSet+QMUI.m in Sources */, CD046C4A2018688F00092035 /* QMUILogger.m in Sources */, + CD513E29283527AA004A549D /* QMUIBarProtocolPrivate.m in Sources */, CDB8CBCC1DCC870800769DF0 /* UIScrollView+QMUI.m in Sources */, CDB8CBA81DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */, CD979996213F934700C00FDC /* QMUIRuntime.m in Sources */, @@ -1623,6 +1657,7 @@ CDC86FFA1F68D63B000E8829 /* QMUIAsset.m in Sources */, CDC86FFB1F68D63B000E8829 /* QMUIAssetsGroup.m in Sources */, CDC86FFC1F68D63B000E8829 /* QMUIAssetsManager.m in Sources */, + CD513E2E283527CE004A549D /* UITabBar+QMUIBarProtocol.m in Sources */, CDC86FFD1F68D63B000E8829 /* QMUIAlbumViewController.m in Sources */, CDC86FFE1F68D63B000E8829 /* QMUIImagePickerCollectionViewCell.m in Sources */, CDC86FFF1F68D63B000E8829 /* QMUIImagePickerHelper.m in Sources */, @@ -1715,6 +1750,7 @@ FECD352722BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m in Sources */, CDC870321F68D63B000E8829 /* QMUITabBarViewController.m in Sources */, D03102B624A8CB410095C232 /* UIView+QMUIBorder.m in Sources */, + CD513E32283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1924,7 +1960,7 @@ "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 4.4.3; + MARKETING_VERSION = 4.5.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1968,7 +2004,7 @@ "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 4.4.3; + MARKETING_VERSION = 4.5.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; From 0e7d2743975fa2e8a650bf794a034d32ab51709a Mon Sep 17 00:00:00 2001 From: molicechen Date: Wed, 10 Aug 2022 15:31:56 +0800 Subject: [PATCH 3/3] 4.5.0 --- QMUIKit.podspec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/QMUIKit.podspec b/QMUIKit.podspec index 4a52c45d..372e29fd 100644 --- a/QMUIKit.podspec +++ b/QMUIKit.podspec @@ -41,6 +41,11 @@ Pod::Spec.new do |s| s.subspec 'QMUIResources' do |ss| ss.resource_bundles = {'QMUIResources' => ['QMUIKit/QMUIResources/*.*']} + ss.pod_target_xcconfig = { + 'EXPANDED_CODE_SIGN_IDENTITY' => '', + 'CODE_SIGNING_REQUIRED' => 'NO', + 'CODE_SIGNING_ALLOWED' => 'NO', + } end s.subspec 'QMUIWeakObjectContainer' do |ss|